diff --git a/anta/input_models/__init__.py b/anta/input_models/__init__.py new file mode 100644 index 000000000..2f674e0a4 --- /dev/null +++ b/anta/input_models/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2023-2024 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. +"""Module related to all ANTA tests input.""" diff --git a/anta/input_models/routing/bgp.py b/anta/input_models/routing/bgp.py new file mode 100644 index 000000000..282738220 --- /dev/null +++ b/anta/input_models/routing/bgp.py @@ -0,0 +1,125 @@ +# Copyright (c) 2023-2024 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. +"""Module containing input models for routing tests.""" + +from __future__ import annotations + +from ipaddress import IPv4Address, IPv6Address +from typing import TYPE_CHECKING, Any +from warnings import warn + +from pydantic import BaseModel, ConfigDict, PositiveInt, model_validator + +from anta.custom_types import Afi, Safi + +if TYPE_CHECKING: + import sys + + if sys.version_info >= (3, 11): + from typing import Self + else: + from typing_extensions import Self + +AFI_SAFI_EOS_KEY = { + ("ipv4", "unicast"): "ipv4Unicast", + ("ipv4", "multicast"): "ipv4Multicast", + ("ipv4", "labeled-unicast"): "ipv4MplsLabels", + ("ipv4", "sr-te"): "ipv4SrTe", + ("ipv6", "unicast"): "ipv6Unicast", + ("ipv6", "multicast"): "ipv6Multicast", + ("ipv6", "labeled-unicast"): "ipv6MplsLabels", + ("ipv6", "sr-te"): "ipv6SrTe", + ("vpn-ipv4", None): "ipv4MplsVpn", + ("vpn-ipv6", None): "ipv6MplsVpn", + ("evpn", None): "l2VpnEvpn", + ("rt-membership", None): "rtMembership", + ("path-selection", None): "dps", + ("link-state", None): "linkState", +} +"""Dictionary mapping AFI/SAFI to EOS key representation.""" + + +class BgpAddressFamily(BaseModel): + """Model for a BGP address family.""" + + model_config = ConfigDict(extra="forbid") + afi: Afi + """BGP Address Family Identifier (AFI).""" + safi: Safi | None = None + """BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`.""" + vrf: str = "default" + """Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`. + + If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`. + + These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6. + """ + num_peers: PositiveInt | None = None + """Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test.""" + peers: list[IPv4Address | IPv6Address] | None = None + """List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test.""" + check_tcp_queues: bool = True + """Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`. + + Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests. + """ + + @model_validator(mode="after") + def validate_inputs(self) -> Self: + """Validate the inputs provided to the BgpAddressFamily class. + + If `afi` is either `ipv4` or `ipv6`, `safi` must be provided. + + If `afi` is not `ipv4` or `ipv6`, `safi` must NOT be provided and `vrf` must be `default`. + """ + if self.afi in ["ipv4", "ipv6"]: + if self.safi is None: + msg = "'safi' must be provided when afi is ipv4 or ipv6" + raise ValueError(msg) + elif self.safi is not None: + msg = "'safi' must not be provided when afi is not ipv4 or ipv6" + raise ValueError(msg) + elif self.vrf != "default": + msg = "'vrf' must be default when afi is not ipv4 or ipv6" + raise ValueError(msg) + return self + + @property + def eos_key(self) -> str: + """AFI/SAFI EOS key representation.""" + # Pydantic handles the validation of the AFI/SAFI combination, so we can ignore error handling here. + return AFI_SAFI_EOS_KEY[(self.afi, self.safi)] + + def __str__(self) -> str: + """Return a string representation of the BgpAddressFamily model. Used in failure messages. + + Examples + -------- + - AFI:ipv4 SAFI:unicast VRF:default + - AFI:evpn + """ + base_string = f"AFI:{self.afi}" + if self.safi is not None: + base_string += f" SAFI:{self.safi}" + if self.afi in ["ipv4", "ipv6"]: + base_string += f" VRF:{self.vrf}" + return base_string + + +class BgpAfi(BgpAddressFamily): + """Alias for the BgpAddressFamily model to maintain backward compatibility. + + When initialized, it will emit a depreciation warning and call the BgpAddressFamily model. + + TODO: Remove this class in ANTA v2.0.0. + """ + + def __init__(self, **data: Any) -> None: # noqa: ANN401 + """Initialize the BgpAfi class, emitting a depreciation warning.""" + warn( + message="BgpAfi model is deprecated and will be removed in ANTA v2.0.0. Use the BgpAddressFamily model instead.", + category=DeprecationWarning, + stacklevel=2, + ) + super().__init__(**data) diff --git a/anta/tests/routing/bgp.py b/anta/tests/routing/bgp.py index a37328608..2a96e4abd 100644 --- a/anta/tests/routing/bgp.py +++ b/anta/tests/routing/bgp.py @@ -7,16 +7,17 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from ipaddress import IPv4Address, IPv4Network, IPv6Address +from ipaddress import IPv4Address, IPv4Network from typing import TYPE_CHECKING, Any, ClassVar -from pydantic import BaseModel, Field, PositiveInt, model_validator +from pydantic import BaseModel, Field, field_validator, model_validator from pydantic.v1.utils import deep_update from pydantic_extra_types.mac_address import MacAddress -from anta.custom_types import Afi, BgpDropStats, BgpUpdateError, MultiProtocolCaps, Safi, Vni +from anta.custom_types import BgpDropStats, BgpUpdateError, MultiProtocolCaps, Vni +from anta.input_models.routing.bgp import BgpAddressFamily, BgpAfi from anta.models import AntaCommand, AntaTemplate, AntaTest -from anta.tools import get_item, get_value +from anta.tools import format_data, get_item, get_value if TYPE_CHECKING: import sys @@ -27,95 +28,6 @@ from typing_extensions import Self -def _add_bgp_failures(failures: dict[tuple[str, str | None], dict[str, Any]], afi: Afi, safi: Safi | None, vrf: str, issue: str | dict[str, Any]) -> None: - """Add a BGP failure entry to the given `failures` dictionary. - - Note: This function modifies `failures` in-place. - - Parameters - ---------- - failures - The dictionary to which the failure will be added. - afi - The address family identifier. - vrf - The VRF name. - safi - The subsequent address family identifier. - issue - A description of the issue. Can be of any type. - - Example - ------- - The `failures` dictionary will have the following structure: - ``` - { - ('afi1', 'safi1'): { - 'afi': 'afi1', - 'safi': 'safi1', - 'vrfs': { - 'vrf1': issue1, - 'vrf2': issue2 - } - }, - ('afi2', None): { - 'afi': 'afi2', - 'vrfs': { - 'vrf1': issue3 - } - } - } - ``` - - """ - key = (afi, safi) - - failure_entry = failures.setdefault(key, {"afi": afi, "safi": safi, "vrfs": {}}) if safi else failures.setdefault(key, {"afi": afi, "vrfs": {}}) - - failure_entry["vrfs"][vrf] = issue - - -def _check_peer_issues(peer_data: dict[str, Any] | None) -> dict[str, Any]: - """Check for issues in BGP peer data. - - Parameters - ---------- - peer_data - The BGP peer data dictionary nested in the `show bgp summary` command. - - Returns - ------- - dict - Dictionary with keys indicating issues or an empty dictionary if no issues. - - Raises - ------ - ValueError - If any of the required keys ("peerState", "inMsgQueue", "outMsgQueue") are missing in `peer_data`, i.e. invalid BGP peer data. - - Example - ------- - This can for instance return - ``` - {"peerNotFound": True} - {"peerState": "Idle", "inMsgQueue": 2, "outMsgQueue": 0} - {} - ``` - - """ - if peer_data is None: - return {"peerNotFound": True} - - if any(key not in peer_data for key in ["peerState", "inMsgQueue", "outMsgQueue"]): - msg = "Provided BGP peer data is invalid." - raise ValueError(msg) - - if peer_data["peerState"] != "Established" or peer_data["inMsgQueue"] != 0 or peer_data["outMsgQueue"] != 0: - return {"peerState": peer_data["peerState"], "inMsgQueue": peer_data["inMsgQueue"], "outMsgQueue": peer_data["outMsgQueue"]} - - return {} - - def _add_bgp_routes_failure( bgp_routes: list[str], bgp_output: dict[str, Any], peer: str, vrf: str, route_type: str = "advertised_routes" ) -> dict[str, dict[str, dict[str, dict[str, list[str]]]]]: @@ -171,19 +83,43 @@ def _add_bgp_routes_failure( return failure_routes -class VerifyBGPPeerCount(AntaTest): - """Verifies the count of BGP peers for a given address family. +def _check_bgp_neighbor_capability(capability_status: dict[str, bool]) -> bool: + """Check if a BGP neighbor capability is advertised, received, and enabled. - It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI). + Parameters + ---------- + capability_status + A dictionary containing the capability status. + + Returns + ------- + bool + True if the capability is advertised, received, and enabled, False otherwise. + + Example + ------- + >>> _check_bgp_neighbor_capability({"advertised": True, "received": True, "enabled": True}) + True + """ + return all(capability_status.get(state, False) for state in ("advertised", "received", "enabled")) + + +class VerifyBGPPeerCount(AntaTest): + """Verifies the count of established BGP peers with negotiated AFI/SAFI for given address families. - For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test. + This test performs the following checks for each specified address family: - Please refer to the Input class attributes below for details. + 1. Confirms that the specified VRF is configured. + 2. Counts the number of peers that are: + - In the `Established` state + - Have successfully negotiated the specified AFI/SAFI Expected Results ---------------- - * Success: If the count of BGP peers matches the expected count for each address family and VRF. - * Failure: If the count of BGP peers does not match the expected count, or if BGP is not configured for an expected VRF or address family. + * Success: If the count of established BGP peers with negotiated AFI/SAFI matches the expected count for all specified address families. + * Failure: If any of the following occur: + - The specified VRF is not configured. + - The count of established peers with negotiated AFI/SAFI does not match the expected count for any address family. Examples -------- @@ -210,129 +146,74 @@ class VerifyBGPPeerCount(AntaTest): """ name = "VerifyBGPPeerCount" - description = "Verifies the count of BGP peers." + description = "Verifies the count of established BGP peers with negotiated AFI/SAFI for given address families." categories: ClassVar[list[str]] = ["bgp"] - commands: ClassVar[list[AntaCommand | AntaTemplate]] = [ - AntaTemplate(template="show bgp {afi} {safi} summary vrf {vrf}", revision=3), - AntaTemplate(template="show bgp {afi} summary", revision=3), - ] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show bgp summary vrf all", revision=1)] class Input(AntaTest.Input): """Input model for the VerifyBGPPeerCount test.""" - address_families: list[BgpAfi] - """List of BGP address families (BgpAfi).""" - - class BgpAfi(BaseModel): - """Model for a BGP address family (AFI) and subsequent address family (SAFI).""" - - afi: Afi - """BGP address family (AFI).""" - safi: Safi | None = None - """Optional BGP subsequent service family (SAFI). - - If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided. - """ - vrf: str = "default" - """ - Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`. - - If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`. - """ - num_peers: PositiveInt - """Number of expected BGP peer(s).""" - - @model_validator(mode="after") - def validate_inputs(self) -> Self: - """Validate the inputs provided to the BgpAfi class. - - If afi is either ipv4 or ipv6, safi must be provided. - - If afi is not ipv4 or ipv6, safi must not be provided and vrf must be default. - """ - if self.afi in ["ipv4", "ipv6"]: - if self.safi is None: - msg = "'safi' must be provided when afi is ipv4 or ipv6" - raise ValueError(msg) - elif self.safi is not None: - msg = "'safi' must not be provided when afi is not ipv4 or ipv6" - raise ValueError(msg) - elif self.vrf != "default": - msg = "'vrf' must be default when afi is not ipv4 or ipv6" + address_families: list[BgpAddressFamily] + """List of BGP address families.""" + BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi + + @field_validator("address_families") + @classmethod + def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]: + """Validate that 'num_peers' field is provided in each address family.""" + for af in address_families: + if af.num_peers is None: + msg = f"{af} 'num_peers' field missing in the input" raise ValueError(msg) - return self - - def render(self, template: AntaTemplate) -> list[AntaCommand]: - """Render the template for each BGP address family in the input list.""" - commands = [] - for afi in self.inputs.address_families: - if template == VerifyBGPPeerCount.commands[0] and afi.afi in ["ipv4", "ipv6"] and afi.safi != "sr-te": - commands.append(template.render(afi=afi.afi, safi=afi.safi, vrf=afi.vrf)) - - # For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 - elif template == VerifyBGPPeerCount.commands[0] and afi.afi in ["ipv4", "ipv6"] and afi.safi == "sr-te": - commands.append(template.render(afi=afi.safi, safi=afi.afi, vrf=afi.vrf)) - elif template == VerifyBGPPeerCount.commands[1] and afi.afi not in ["ipv4", "ipv6"]: - commands.append(template.render(afi=afi.afi)) - return commands + return address_families @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyBGPPeerCount.""" self.result.is_success() - failures: dict[tuple[str, Any], dict[str, Any]] = {} + output = self.instance_commands[0].json_output - for command in self.instance_commands: - num_peers = None + for address_family in self.inputs.address_families: peer_count = 0 - command_output = command.json_output - - afi = command.params.afi - safi = command.params.safi if hasattr(command.params, "safi") else None - afi_vrf = command.params.vrf if hasattr(command.params, "vrf") else "default" - # Swapping AFI and SAFI in case of SR-TE - if afi == "sr-te": - afi, safi = safi, afi - - for input_entry in self.inputs.address_families: - if input_entry.afi == afi and input_entry.safi == safi and input_entry.vrf == afi_vrf: - num_peers = input_entry.num_peers - break - - if not (vrfs := command_output.get("vrfs")): - _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue="Not Configured") + # Check if the VRF is configured + if (vrf_output := get_value(output, f"vrfs.{address_family.vrf}")) is None: + self.result.is_failure(f"{address_family} - VRF not configured") continue - if afi_vrf == "all": - for vrf_data in vrfs.values(): - peer_count += len(vrf_data["peers"]) - else: - peer_count += len(command_output["vrfs"][afi_vrf]["peers"]) - - if peer_count != num_peers: - _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=f"Expected: {num_peers}, Actual: {peer_count}") + # Count the number of established peers with negotiated AFI/SAFI + for peer_data in vrf_output.get("peers", {}).values(): + if peer_data.get("peerState") == "Established" and get_value(peer_data, f"{address_family.eos_key}.afiSafiState") == "negotiated": + peer_count += 1 - if failures: - self.result.is_failure(f"Failures: {list(failures.values())}") + # Check if the count matches the expected count + if address_family.num_peers != peer_count: + self.result.is_failure(f"{address_family} - Expected: {address_family.num_peers}, Actual: {peer_count}") class VerifyBGPPeersHealth(AntaTest): - """Verifies the health of BGP peers. - - It will validate that all BGP sessions are established and all message queues for these BGP sessions are empty for a given address family. + """Verifies the health of BGP peers for given address families. - It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI). + This test performs the following checks for each specified address family: - For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test. - - Please refer to the Input class attributes below for details. + 1. Validates that the VRF is configured. + 2. Checks if there are any peers for the given AFI/SAFI. + 3. For each relevant peer: + - Verifies that the BGP session is in the `Established` state. + - Confirms that the AFI/SAFI state is `negotiated`. + - Checks that both input and output TCP message queues are empty. + Can be disabled by setting `check_tcp_queues` to `False`. Expected Results ---------------- - * Success: If all BGP sessions are established and all messages queues are empty for each address family and VRF. - * Failure: If there are issues with any of the BGP sessions, or if BGP is not configured for an expected VRF or address family. + * Success: If all checks pass for all specified address families and their peers. + * Failure: If any of the following occur: + - The specified VRF is not configured. + - No peers are found for a given AFI/SAFI. + - Any BGP session is not in the `Established` state. + - The AFI/SAFI state is not 'negotiated' for any peer. + - Any TCP message queue (input or output) is not empty when `check_tcp_queues` is `True` (default). Examples -------- @@ -348,130 +229,83 @@ class VerifyBGPPeersHealth(AntaTest): - afi: "ipv6" safi: "unicast" vrf: "DEV" + check_tcp_queues: false ``` """ name = "VerifyBGPPeersHealth" - description = "Verifies the health of BGP peers" + description = "Verifies the health of BGP peers for given address families." categories: ClassVar[list[str]] = ["bgp"] - commands: ClassVar[list[AntaCommand | AntaTemplate]] = [ - AntaTemplate(template="show bgp {afi} {safi} summary vrf {vrf}", revision=3), - AntaTemplate(template="show bgp {afi} summary", revision=3), - ] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show bgp neighbors vrf all", revision=3)] class Input(AntaTest.Input): """Input model for the VerifyBGPPeersHealth test.""" - address_families: list[BgpAfi] - """List of BGP address families (BgpAfi).""" - - class BgpAfi(BaseModel): - """Model for a BGP address family (AFI) and subsequent address family (SAFI).""" - - afi: Afi - """BGP address family (AFI).""" - safi: Safi | None = None - """Optional BGP subsequent service family (SAFI). - - If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided. - """ - vrf: str = "default" - """ - Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`. - - If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`. - """ - - @model_validator(mode="after") - def validate_inputs(self) -> Self: - """Validate the inputs provided to the BgpAfi class. - - If afi is either ipv4 or ipv6, safi must be provided. - - If afi is not ipv4 or ipv6, safi must not be provided and vrf must be default. - """ - if self.afi in ["ipv4", "ipv6"]: - if self.safi is None: - msg = "'safi' must be provided when afi is ipv4 or ipv6" - raise ValueError(msg) - elif self.safi is not None: - msg = "'safi' must not be provided when afi is not ipv4 or ipv6" - raise ValueError(msg) - elif self.vrf != "default": - msg = "'vrf' must be default when afi is not ipv4 or ipv6" - raise ValueError(msg) - return self - - def render(self, template: AntaTemplate) -> list[AntaCommand]: - """Render the template for each BGP address family in the input list.""" - commands = [] - for afi in self.inputs.address_families: - if template == VerifyBGPPeersHealth.commands[0] and afi.afi in ["ipv4", "ipv6"] and afi.safi != "sr-te": - commands.append(template.render(afi=afi.afi, safi=afi.safi, vrf=afi.vrf)) - - # For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 - elif template == VerifyBGPPeersHealth.commands[0] and afi.afi in ["ipv4", "ipv6"] and afi.safi == "sr-te": - commands.append(template.render(afi=afi.safi, safi=afi.afi, vrf=afi.vrf)) - elif template == VerifyBGPPeersHealth.commands[1] and afi.afi not in ["ipv4", "ipv6"]: - commands.append(template.render(afi=afi.afi)) - return commands + address_families: list[BgpAddressFamily] + """List of BGP address families.""" + BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyBGPPeersHealth.""" self.result.is_success() - failures: dict[tuple[str, Any], dict[str, Any]] = {} - - for command in self.instance_commands: - command_output = command.json_output - - afi = command.params.afi - safi = command.params.safi if hasattr(command.params, "safi") else None - afi_vrf = command.params.vrf if hasattr(command.params, "vrf") else "default" - - # Swapping AFI and SAFI in case of SR-TE - if afi == "sr-te": - afi, safi = safi, afi + output = self.instance_commands[0].json_output - if not (vrfs := command_output.get("vrfs")): - _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue="Not Configured") + for address_family in self.inputs.address_families: + # Check if the VRF is configured + if (vrf_output := get_value(output, f"vrfs.{address_family.vrf}")) is None: + self.result.is_failure(f"{address_family} - VRF not configured") continue - for vrf, vrf_data in vrfs.items(): - if not (peers := vrf_data.get("peers")): - _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue="No Peers") - continue + # Check if any peers are found for this AFI/SAFI + relevant_peers = [ + peer for peer in vrf_output.get("peerList", []) if get_value(peer, f"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}") is not None + ] - peer_issues = {} - for peer, peer_data in peers.items(): - issues = _check_peer_issues(peer_data) + if not relevant_peers: + self.result.is_failure(f"{address_family} - No peers found") + continue - if issues: - peer_issues[peer] = issues + for peer in relevant_peers: + # Check if the BGP session is established + if peer["state"] != "Established": + self.result.is_failure(f"{address_family} - Peer:{peer['peerAddress']} session state is not established; State:{peer['state']}") - if peer_issues: - _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=vrf, issue=peer_issues) + # Check if the AFI/SAFI state is negotiated + capability_status = get_value(peer, f"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}") + if not _check_bgp_neighbor_capability(capability_status): + self.result.is_failure(f"{address_family} - Peer:{peer['peerAddress']} AFI/SAFI state is not negotiated; {format_data(capability_status)}") - if failures: - self.result.is_failure(f"Failures: {list(failures.values())}") + # Check the TCP session message queues + inq = peer["peerTcpInfo"]["inputQueueLength"] + outq = peer["peerTcpInfo"]["outputQueueLength"] + if address_family.check_tcp_queues and (inq != 0 or outq != 0): + self.result.is_failure(f"{address_family} - Peer:{peer['peerAddress']} session has non-empty message queues; InQ: {inq}, OutQ: {outq}") class VerifyBGPSpecificPeers(AntaTest): - """Verifies the health of specific BGP peer(s). - - It will validate that the BGP session is established and all message queues for this BGP session are empty for the given peer(s). + """Verifies the health of specific BGP peer(s) for given address families. - It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI). + This test performs the following checks for each specified address family and peer: - For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test. - - Please refer to the Input class attributes below for details. + 1. Confirms that the specified VRF is configured. + 2. For each specified peer: + - Verifies that the peer is found in the BGP configuration. + - Checks that the BGP session is in the `Established` state. + - Confirms that the AFI/SAFI state is `negotiated`. + - Ensures that both input and output TCP message queues are empty. + Can be disabled by setting `check_tcp_queues` to `False`. Expected Results ---------------- - * Success: If the BGP session is established and all messages queues are empty for each given peer. - * Failure: If the BGP session has issues or is not configured, or if BGP is not configured for an expected VRF or address family. + * Success: If all checks pass for all specified peers in all address families. + * Failure: If any of the following occur: + - The specified VRF is not configured. + - A specified peer is not found in the BGP configuration. + - The BGP session for a peer is not in the `Established` state. + - The AFI/SAFI state is not `negotiated` for a peer. + - Any TCP message queue (input or output) is not empty for a peer when `check_tcp_queues` is `True` (default). Examples -------- @@ -497,116 +331,63 @@ class VerifyBGPSpecificPeers(AntaTest): name = "VerifyBGPSpecificPeers" description = "Verifies the health of specific BGP peer(s)." categories: ClassVar[list[str]] = ["bgp"] - commands: ClassVar[list[AntaCommand | AntaTemplate]] = [ - AntaTemplate(template="show bgp {afi} {safi} summary vrf {vrf}", revision=3), - AntaTemplate(template="show bgp {afi} summary", revision=3), - ] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show bgp neighbors vrf all", revision=3)] class Input(AntaTest.Input): """Input model for the VerifyBGPSpecificPeers test.""" - address_families: list[BgpAfi] - """List of BGP address families (BgpAfi).""" - - class BgpAfi(BaseModel): - """Model for a BGP address family (AFI) and subsequent address family (SAFI).""" - - afi: Afi - """BGP address family (AFI).""" - safi: Safi | None = None - """Optional BGP subsequent service family (SAFI). - - If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided. - """ - vrf: str = "default" - """ - Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`. - - `all` is NOT supported. - - If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`. - """ - peers: list[IPv4Address | IPv6Address] - """List of BGP IPv4 or IPv6 peer.""" - - @model_validator(mode="after") - def validate_inputs(self) -> Self: - """Validate the inputs provided to the BgpAfi class. - - If afi is either ipv4 or ipv6, safi must be provided and vrf must NOT be all. - - If afi is not ipv4 or ipv6, safi must not be provided and vrf must be default. - """ - if self.afi in ["ipv4", "ipv6"]: - if self.safi is None: - msg = "'safi' must be provided when afi is ipv4 or ipv6" - raise ValueError(msg) - if self.vrf == "all": - msg = "'all' is not supported in this test. Use VerifyBGPPeersHealth test instead." - raise ValueError(msg) - elif self.safi is not None: - msg = "'safi' must not be provided when afi is not ipv4 or ipv6" + address_families: list[BgpAddressFamily] + """List of BGP address families.""" + BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi + + @field_validator("address_families") + @classmethod + def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]: + """Validate that 'peers' field is provided in each address family.""" + for af in address_families: + if af.peers is None: + msg = f"{af} 'peers' field missing in the input" raise ValueError(msg) - elif self.vrf != "default": - msg = "'vrf' must be default when afi is not ipv4 or ipv6" - raise ValueError(msg) - return self - - def render(self, template: AntaTemplate) -> list[AntaCommand]: - """Render the template for each BGP address family in the input list.""" - commands = [] - - for afi in self.inputs.address_families: - if template == VerifyBGPSpecificPeers.commands[0] and afi.afi in ["ipv4", "ipv6"] and afi.safi != "sr-te": - commands.append(template.render(afi=afi.afi, safi=afi.safi, vrf=afi.vrf)) - - # For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 - elif template == VerifyBGPSpecificPeers.commands[0] and afi.afi in ["ipv4", "ipv6"] and afi.safi == "sr-te": - commands.append(template.render(afi=afi.safi, safi=afi.afi, vrf=afi.vrf)) - elif template == VerifyBGPSpecificPeers.commands[1] and afi.afi not in ["ipv4", "ipv6"]: - commands.append(template.render(afi=afi.afi)) - return commands + return address_families @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyBGPSpecificPeers.""" self.result.is_success() - failures: dict[tuple[str, Any], dict[str, Any]] = {} + output = self.instance_commands[0].json_output - for command in self.instance_commands: - command_output = command.json_output - - afi = command.params.afi - safi = command.params.safi if hasattr(command.params, "safi") else None - afi_vrf = command.params.vrf if hasattr(command.params, "vrf") else "default" + for address_family in self.inputs.address_families: + # Check if the VRF is configured + if (vrf_output := get_value(output, f"vrfs.{address_family.vrf}")) is None: + self.result.is_failure(f"{address_family} - VRF not configured") + continue - # Swapping AFI and SAFI in case of SR-TE - if afi == "sr-te": - afi, safi = safi, afi + for peer in address_family.peers: + peer_ip = str(peer) - for input_entry in self.inputs.address_families: - if input_entry.afi == afi and input_entry.safi == safi and input_entry.vrf == afi_vrf: - afi_peers = input_entry.peers - break + # Check if the peer is found + if (peer_data := get_item(vrf_output["peerList"], "peerAddress", peer_ip)) is None: + self.result.is_failure(f"{address_family} - Peer:{peer_ip} not found") + continue - if not (vrfs := command_output.get("vrfs")): - _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue="Not Configured") - continue + # Check if the BGP session is established + if peer_data["state"] != "Established": + self.result.is_failure(f"{address_family} - Peer:{peer_ip} session state is not established; State:{peer_data['state']}") - peer_issues = {} - for peer in afi_peers: - peer_ip = str(peer) - peer_data = get_value(dictionary=vrfs, key=f"{afi_vrf}_peers_{peer_ip}", separator="_") - issues = _check_peer_issues(peer_data) - if issues: - peer_issues[peer_ip] = issues + # Check if the AFI/SAFI state is negotiated + capability_status = get_value(peer_data, f"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}") + if not capability_status: + self.result.is_failure(f"{address_family} Peer:{peer_ip} AFI/SAFI state is not negotiated") - if peer_issues: - _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=peer_issues) + if capability_status and not _check_bgp_neighbor_capability(capability_status): + self.result.is_failure(f"{address_family} Peer:{peer_ip} AFI/SAFI state is not negotiated; {format_data(capability_status)}") - if failures: - self.result.is_failure(f"Failures: {list(failures.values())}") + # Check the TCP session message queues + inq = peer_data["peerTcpInfo"]["inputQueueLength"] + outq = peer_data["peerTcpInfo"]["outputQueueLength"] + if address_family.check_tcp_queues and (inq != 0 or outq != 0): + self.result.is_failure(f"{address_family} - Peer:{peer_ip} session has non-empty message queues; InQ: {inq}, OutQ: {outq}") class VerifyBGPExchangedRoutes(AntaTest): diff --git a/anta/tools.py b/anta/tools.py index 4f73db9cb..c2a12956b 100644 --- a/anta/tools.py +++ b/anta/tools.py @@ -378,7 +378,7 @@ def safe_command(command: str) -> str: def convert_categories(categories: list[str]) -> list[str]: """Convert categories for reports. - if the category is part of the defined acronym, transform it to upper case + If the category is part of the defined acronym, transform it to upper case otherwise capitalize the first letter. Parameters @@ -395,3 +395,24 @@ def convert_categories(categories: list[str]) -> list[str]: return [" ".join(word.upper() if word.lower() in ACRONYM_CATEGORIES else word.title() for word in category.split()) for category in categories] msg = f"Wrong input type '{type(categories)}' for convert_categories." raise TypeError(msg) + + +def format_data(data: dict[str, bool]) -> str: + """Format a data dictionary for logging purposes. + + Parameters + ---------- + data + A dictionary containing the data to format. + + Returns + ------- + str + The formatted data. + + Example + ------- + >>> format_data({"advertised": True, "received": True, "enabled": True}) + "Advertised:True, Received:True, Enabled:True" + """ + return ", ".join(f"{k.capitalize()}:{v}" for k, v in data.items()) diff --git a/tests/units/anta_tests/routing/test_bgp.py b/tests/units/anta_tests/routing/test_bgp.py index e256b04dd..3d7d7e0f1 100644 --- a/tests/units/anta_tests/routing/test_bgp.py +++ b/tests/units/anta_tests/routing/test_bgp.py @@ -32,715 +32,466 @@ "name": "success", "test": VerifyBGPPeerCount, "eos_data": [ - # Need to order the output as the commands would be sorted after template rendering. { "vrfs": { "default": { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": "65120", "peers": { - "10.1.255.0": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.255.2": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - }, - }, - }, - { - "vrfs": { - "MGMT": { - "peers": { - "10.255.0.21": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.0.1": { "peerState": "Established", + "peerAsn": "65100", + "ipv4MplsVpn": {"afiSafiState": "advertised", "nlrisReceived": 0, "nlrisAccepted": 0}, + "l2VpnEvpn": {"afiSafiState": "negotiated", "nlrisReceived": 42, "nlrisAccepted": 42}, }, - }, - }, - }, - }, - { - "vrfs": { - "default": { - "peers": { - "10.255.0.1": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.0.2": { "peerState": "Established", + "peerAsn": "65100", + "ipv4MplsVpn": {"afiSafiState": "advertised", "nlrisReceived": 0, "nlrisAccepted": 0}, + "l2VpnEvpn": {"afiSafiState": "negotiated", "nlrisReceived": 42, "nlrisAccepted": 42}, }, - "10.255.0.2": { - "description": "DC1-SPINE2_Ethernet1", - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.254.1": { "peerState": "Established", + "peerAsn": "65120", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 17, "nlrisAccepted": 17}, }, - }, - }, - }, - }, - { - "vrfs": { - "default": { - "peers": { - "10.255.0.11": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.255.0": { "peerState": "Established", + "peerAsn": "65100", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 14, "nlrisAccepted": 14}, }, - "10.255.0.12": { - "description": "DC1-SPINE2_Ethernet1", - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.255.2": { "peerState": "Established", + "peerAsn": "65100", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 14, "nlrisAccepted": 14}, }, }, }, - }, - }, - { - "vrfs": { - "default": { + "DEV": { + "vrf": "DEV", + "routerId": "10.1.0.3", + "asn": "65120", "peers": { - "10.255.0.21": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.255.0.22": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.254.1": { "peerState": "Established", - }, + "peerAsn": "65120", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 4, "nlrisAccepted": 4}, + } }, }, - }, + } }, ], "inputs": { "address_families": [ - # evpn first to make sure that the correct mapping output to input is kept. {"afi": "evpn", "num_peers": 2}, - {"afi": "ipv4", "safi": "unicast", "vrf": "default", "num_peers": 2}, - {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT", "num_peers": 1}, - {"afi": "link-state", "num_peers": 2}, - {"afi": "path-selection", "num_peers": 2}, + {"afi": "ipv4", "safi": "unicast", "vrf": "default", "num_peers": 3}, + {"afi": "ipv4", "safi": "unicast", "vrf": "DEV", "num_peers": 1}, ] }, "expected": {"result": "success"}, }, { - "name": "failure-wrong-count", + "name": "failure-vrf-not-configured", "test": VerifyBGPPeerCount, "eos_data": [ { "vrfs": { "default": { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": "65120", "peers": { - "10.1.255.0": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.255.2": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - }, - }, - }, - { - "vrfs": { - "MGMT": { - "peers": { - "10.255.0.21": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.0.1": { "peerState": "Established", + "peerAsn": "65100", + "ipv4MplsVpn": {"afiSafiState": "advertised", "nlrisReceived": 0, "nlrisAccepted": 0}, + "l2VpnEvpn": {"afiSafiState": "negotiated", "nlrisReceived": 42, "nlrisAccepted": 42}, }, - }, - }, - }, - }, - { - "vrfs": { - "default": { - "peers": { - "10.255.0.1": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.0.2": { "peerState": "Established", + "peerAsn": "65100", + "ipv4MplsVpn": {"afiSafiState": "advertised", "nlrisReceived": 0, "nlrisAccepted": 0}, + "l2VpnEvpn": {"afiSafiState": "negotiated", "nlrisReceived": 42, "nlrisAccepted": 42}, }, - "10.255.0.2": { - "description": "DC1-SPINE2_Ethernet1", - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.254.1": { "peerState": "Established", + "peerAsn": "65120", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 17, "nlrisAccepted": 17}, }, - }, - }, - }, - }, - { - "vrfs": { - "default": { - "peers": { - "10.255.0.11": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.255.0": { "peerState": "Established", + "peerAsn": "65100", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 14, "nlrisAccepted": 14}, }, - "10.255.0.12": { - "description": "DC1-SPINE2_Ethernet1", - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.255.2": { "peerState": "Established", + "peerAsn": "65100", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 14, "nlrisAccepted": 14}, }, }, }, - }, - }, - { - "vrfs": { - "default": { + "DEV": { + "vrf": "DEV", + "routerId": "10.1.0.3", + "asn": "65120", "peers": { - "10.255.0.21": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.254.1": { "peerState": "Established", - }, + "peerAsn": "65120", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 4, "nlrisAccepted": 4}, + } }, }, - }, - }, - ], - "inputs": { - "address_families": [ - {"afi": "ipv4", "safi": "unicast", "vrf": "default", "num_peers": 3}, - {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT", "num_peers": 2}, - {"afi": "evpn", "num_peers": 1}, - {"afi": "link-state", "num_peers": 3}, - {"afi": "path-selection", "num_peers": 3}, - ] - }, - "expected": { - "result": "failure", - "messages": [ - "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'default': 'Expected: 3, Actual: 2'}}, " - "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'Expected: 2, Actual: 1'}}, " - "{'afi': 'evpn', 'vrfs': {'default': 'Expected: 1, Actual: 2'}}, " - "{'afi': 'link-state', 'vrfs': {'default': 'Expected: 3, Actual: 2'}}, " - "{'afi': 'path-selection', 'vrfs': {'default': 'Expected: 3, Actual: 1'}}]" - ], - }, - }, - { - "name": "failure-no-peers", - "test": VerifyBGPPeerCount, - "eos_data": [ - { - "vrfs": { - "default": { - "peers": {}, - } - } - }, - { - "vrfs": { - "MGMT": { - "peers": {}, - } - } - }, - { - "vrfs": { - "default": { - "peers": {}, - } - } - }, - { - "vrfs": { - "default": { - "peers": {}, - } - } - }, - { - "vrfs": { - "default": { - "peers": {}, - } } }, ], "inputs": { "address_families": [ - {"afi": "ipv4", "safi": "unicast", "vrf": "default", "num_peers": 2}, - {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT", "num_peers": 1}, - {"afi": "evpn", "num_peers": 2}, - {"afi": "link-state", "num_peers": 2}, - {"afi": "path-selection", "num_peers": 2}, - ] - }, - "expected": { - "result": "failure", - "messages": [ - "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'default': 'Expected: 2, Actual: 0'}}, " - "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'Expected: 1, Actual: 0'}}, " - "{'afi': 'evpn', 'vrfs': {'default': 'Expected: 2, Actual: 0'}}, " - "{'afi': 'link-state', 'vrfs': {'default': 'Expected: 2, Actual: 0'}}, " - "{'afi': 'path-selection', 'vrfs': {'default': 'Expected: 2, Actual: 0'}}]" - ], - }, - }, - { - "name": "failure-not-configured", - "test": VerifyBGPPeerCount, - "eos_data": [{"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}], - "inputs": { - "address_families": [ - {"afi": "ipv6", "safi": "multicast", "vrf": "DEV", "num_peers": 3}, - {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT", "num_peers": 1}, {"afi": "evpn", "num_peers": 2}, - {"afi": "link-state", "num_peers": 2}, - {"afi": "path-selection", "num_peers": 2}, + {"afi": "ipv4", "safi": "unicast", "vrf": "default", "num_peers": 3}, + {"afi": "ipv4", "safi": "unicast", "vrf": "PROD", "num_peers": 2}, ] }, "expected": { "result": "failure", "messages": [ - "Failures: [{'afi': 'ipv6', 'safi': 'multicast', 'vrfs': {'DEV': 'Not Configured'}}, " - "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'Not Configured'}}, " - "{'afi': 'evpn', 'vrfs': {'default': 'Not Configured'}}, " - "{'afi': 'link-state', 'vrfs': {'default': 'Not Configured'}}, " - "{'afi': 'path-selection', 'vrfs': {'default': 'Not Configured'}}]" + "AFI:ipv4 SAFI:unicast VRF:PROD - VRF not configured", ], }, }, { - "name": "success-vrf-all", + "name": "failure-afi-safi-not-negotiated", "test": VerifyBGPPeerCount, "eos_data": [ { "vrfs": { "default": { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": "65120", "peers": { - "10.1.255.0": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.0.1": { "peerState": "Established", + "peerAsn": "65100", + "ipv4MplsVpn": {"afiSafiState": "advertised", "nlrisReceived": 0, "nlrisAccepted": 0}, + "l2VpnEvpn": {"afiSafiState": "negotiated", "nlrisReceived": 42, "nlrisAccepted": 42}, + }, + "10.1.0.2": { + "peerState": "Established", + "peerAsn": "65100", + "ipv4MplsVpn": {"afiSafiState": "advertised", "nlrisReceived": 0, "nlrisAccepted": 0}, + "l2VpnEvpn": {"afiSafiState": "negotiated", "nlrisReceived": 42, "nlrisAccepted": 42}, }, - }, - }, - "PROD": { - "peers": { "10.1.254.1": { - "inMsgQueue": 0, - "outMsgQueue": 0, "peerState": "Established", + "peerAsn": "65120", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 17, "nlrisAccepted": 17}, }, - "192.168.1.11": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.255.0": { "peerState": "Established", + "peerAsn": "65100", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 14, "nlrisAccepted": 14}, }, - }, - }, - }, - }, - { - "vrfs": { - "default": { - "peers": { - "10.1.255.10": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.255.2": { "peerState": "Established", + "peerAsn": "65100", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 14, "nlrisAccepted": 14}, }, }, }, - "PROD": { + "DEV": { + "vrf": "DEV", + "routerId": "10.1.0.3", + "asn": "65120", "peers": { - "10.1.254.11": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.254.1": { "peerState": "Established", - }, + "peerAsn": "65120", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 4, "nlrisAccepted": 4}, + } }, }, - }, + } }, ], "inputs": { "address_families": [ - {"afi": "ipv4", "safi": "unicast", "vrf": "all", "num_peers": 3}, - {"afi": "ipv4", "safi": "sr-te", "vrf": "all", "num_peers": 2}, + {"afi": "evpn", "num_peers": 2}, + {"afi": "vpn-ipv4", "num_peers": 2}, + {"afi": "ipv4", "safi": "unicast", "vrf": "default", "num_peers": 3}, + {"afi": "ipv4", "safi": "unicast", "vrf": "DEV", "num_peers": 1}, ] }, - "expected": {"result": "success"}, + "expected": { + "result": "failure", + "messages": [ + "AFI:vpn-ipv4 - Expected: 2, Actual: 0", + ], + }, }, { - "name": "failure-vrf-all", + "name": "failure-wrong-count", "test": VerifyBGPPeerCount, "eos_data": [ { "vrfs": { "default": { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": "65120", "peers": { - "10.1.255.0": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.0.1": { "peerState": "Established", + "peerAsn": "65100", + "ipv4MplsVpn": {"afiSafiState": "advertised", "nlrisReceived": 0, "nlrisAccepted": 0}, + "l2VpnEvpn": {"afiSafiState": "negotiated", "nlrisReceived": 42, "nlrisAccepted": 42}, + }, + "10.1.0.2": { + "peerState": "Established", + "peerAsn": "65100", + "ipv4MplsVpn": {"afiSafiState": "advertised", "nlrisReceived": 0, "nlrisAccepted": 0}, + "l2VpnEvpn": {"afiSafiState": "negotiated", "nlrisReceived": 42, "nlrisAccepted": 42}, }, - }, - }, - "PROD": { - "peers": { "10.1.254.1": { - "inMsgQueue": 0, - "outMsgQueue": 0, "peerState": "Established", + "peerAsn": "65120", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 17, "nlrisAccepted": 17}, }, - "192.168.1.11": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.255.0": { "peerState": "Established", + "peerAsn": "65100", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 14, "nlrisAccepted": 14}, }, - }, - }, - }, - }, - { - "vrfs": { - "default": { - "peers": { - "10.1.255.10": { - "inMsgQueue": 0, - "outMsgQueue": 0, + "10.1.255.2": { "peerState": "Established", + "peerAsn": "65100", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 14, "nlrisAccepted": 14}, }, }, }, - "PROD": { + "DEV": { + "vrf": "DEV", + "routerId": "10.1.0.3", + "asn": "65120", "peers": { "10.1.254.1": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "192.168.1.12": { - "inMsgQueue": 0, - "outMsgQueue": 0, "peerState": "Established", - }, + "peerAsn": "65120", + "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 4, "nlrisAccepted": 4}, + } }, }, - }, + } }, ], "inputs": { "address_families": [ - {"afi": "ipv4", "safi": "unicast", "vrf": "all", "num_peers": 5}, - {"afi": "ipv4", "safi": "sr-te", "vrf": "all", "num_peers": 2}, + {"afi": "evpn", "num_peers": 3}, + {"afi": "ipv4", "safi": "unicast", "vrf": "default", "num_peers": 3}, + {"afi": "ipv4", "safi": "unicast", "vrf": "DEV", "num_peers": 2}, ] }, "expected": { "result": "failure", "messages": [ - "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'all': 'Expected: 5, Actual: 3'}}, " - "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'all': 'Expected: 2, Actual: 3'}}]" + "AFI:evpn - Expected: 3, Actual: 2", + "AFI:ipv4 SAFI:unicast VRF:DEV - Expected: 2, Actual: 1", ], }, }, { - "name": "failure-multiple-afi", - "test": VerifyBGPPeerCount, + "name": "success", + "test": VerifyBGPPeersHealth, "eos_data": [ { "vrfs": { - "PROD": { - "peers": { - "10.1.254.1": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + "default": { + "peerList": [ + { + "peerAddress": "10.100.0.12", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - "192.168.1.11": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + { + "peerAddress": "10.100.0.13", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"l2VpnEvpn": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - }, + ] }, - }, - }, - {"vrfs": {}}, - { - "vrfs": { - "MGMT": { - "peers": { - "10.1.254.11": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + "DEV": { + "peerList": [ + { + "peerAddress": "10.100.0.12", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - "192.168.1.21": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - }, - }, - }, - { - "vrfs": { - "default": { - "peers": { - "10.1.0.1": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.0.2": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - }, - }, - }, - { - "vrfs": { - "default": { - "peers": { - "10.1.0.11": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.0.21": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, + ] }, - }, - }, + } + } + ], + "inputs": { + "address_families": [ + {"afi": "evpn"}, + {"afi": "ipv4", "safi": "unicast", "vrf": "default"}, + {"afi": "ipv4", "safi": "unicast", "vrf": "DEV"}, + ] + }, + "expected": {"result": "success"}, + }, + { + "name": "failure-vrf-not-configured", + "test": VerifyBGPPeersHealth, + "eos_data": [ { - "vrfs": { - "default": { - "peers": { - "10.1.0.2": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.0.22": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - }, - }, - }, + "vrfs": {}, + } ], "inputs": { "address_families": [ - {"afi": "ipv4", "safi": "unicast", "vrf": "PROD", "num_peers": 3}, - {"afi": "ipv6", "safi": "unicast", "vrf": "default", "num_peers": 3}, - {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT", "num_peers": 3}, - {"afi": "evpn", "num_peers": 3}, - {"afi": "link-state", "num_peers": 4}, - {"afi": "path-selection", "num_peers": 1}, + {"afi": "ipv4", "safi": "unicast", "vrf": "default"}, + {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT"}, + {"afi": "path-selection"}, + {"afi": "link-state"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "AFI:ipv4 SAFI:unicast VRF:default - VRF not configured", + "AFI:ipv4 SAFI:sr-te VRF:MGMT - VRF not configured", + "AFI:path-selection - VRF not configured", + "AFI:link-state - VRF not configured", ], }, + }, + { + "name": "failure-peer-not-found", + "test": VerifyBGPPeersHealth, + "eos_data": [{"vrfs": {"default": {"peerList": []}, "MGMT": {"peerList": []}}}], + "inputs": { + "address_families": [ + {"afi": "ipv4", "safi": "unicast", "vrf": "default"}, + {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT"}, + {"afi": "path-selection"}, + {"afi": "link-state"}, + ] + }, "expected": { "result": "failure", "messages": [ - "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'PROD': 'Expected: 3, Actual: 2'}}, " - "{'afi': 'ipv6', 'safi': 'unicast', 'vrfs': {'default': 'Not Configured'}}, " - "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'Expected: 3, Actual: 2'}}, " - "{'afi': 'evpn', 'vrfs': {'default': 'Expected: 3, Actual: 2'}}, " - "{'afi': 'link-state', 'vrfs': {'default': 'Expected: 4, Actual: 2'}}, " - "{'afi': 'path-selection', 'vrfs': {'default': 'Expected: 1, Actual: 2'}}]", + "AFI:ipv4 SAFI:unicast VRF:default - No peers found", + "AFI:ipv4 SAFI:sr-te VRF:MGMT - No peers found", + "AFI:path-selection - No peers found", + "AFI:link-state - No peers found", ], }, }, { - "name": "success", + "name": "failure-session-not-established", "test": VerifyBGPPeersHealth, "eos_data": [ { "vrfs": { "default": { - "peers": { - "10.1.255.0": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.255.2": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - } - } - }, - { - "vrfs": { - "MGMT": { - "peers": { - "10.1.255.10": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.255.12": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - } - } - }, - { - "vrfs": { - "default": { - "peers": { - "10.1.255.20": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + "peerList": [ + { + "peerAddress": "10.100.0.12", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - "10.1.255.22": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + { + "peerAddress": "10.100.0.13", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"dps": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - }, - } - } - }, - { - "vrfs": { - "default": { - "peers": { - "10.1.255.30": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + { + "peerAddress": "10.100.0.14", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"linkState": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - "10.1.255.32": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.12", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4SrTe": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - }, - } + ] + }, } - }, + } ], "inputs": { "address_families": [ - # Path selection first to make sure input to output mapping is correct. - {"afi": "path-selection"}, {"afi": "ipv4", "safi": "unicast", "vrf": "default"}, {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT"}, + {"afi": "path-selection"}, {"afi": "link-state"}, ] }, - "expected": {"result": "success"}, + "expected": { + "result": "failure", + "messages": [ + "AFI:ipv4 SAFI:unicast VRF:default - Peer:10.100.0.12 session state is not established; State:Idle", + "AFI:ipv4 SAFI:sr-te VRF:MGMT - Peer:10.100.0.12 session state is not established; State:Idle", + "AFI:path-selection - Peer:10.100.0.13 session state is not established; State:Idle", + "AFI:link-state - Peer:10.100.0.14 session state is not established; State:Idle", + ], + }, }, { - "name": "failure-issues", + "name": "failure-afi-not-negotiated", "test": VerifyBGPPeersHealth, "eos_data": [ { "vrfs": { "default": { - "peers": { - "10.1.255.0": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Idle", - }, - "10.1.255.2": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - } - } - }, - { - "vrfs": { - "MGMT": { - "peers": { - "10.1.255.10": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.255.12": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Idle", - }, - }, - } - } - }, - { - "vrfs": { - "default": { - "peers": { - "10.1.255.20": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Idle", + "peerList": [ + { + "peerAddress": "10.100.0.12", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": False, "received": False, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - "10.1.255.22": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + { + "peerAddress": "10.100.0.13", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"dps": {"advertised": True, "received": False, "enabled": False}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - }, - } - } - }, - { - "vrfs": { - "default": { - "peers": { - "10.1.255.30": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + { + "peerAddress": "10.100.0.14", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"linkState": {"advertised": False, "received": False, "enabled": False}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - "10.1.255.32": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Idle", + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.12", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4SrTe": {"advertised": False, "received": False, "enabled": False}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - }, - } + ] + }, } - }, + } ], "inputs": { "address_families": [ @@ -753,559 +504,362 @@ "expected": { "result": "failure", "messages": [ - "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'default': {'10.1.255.0': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}, " - "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': {'10.1.255.12': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}, " - "{'afi': 'path-selection', 'vrfs': {'default': {'10.1.255.20': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}, " - "{'afi': 'link-state', 'vrfs': {'default': {'10.1.255.32': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}]" + "AFI:ipv4 SAFI:unicast VRF:default - Peer:10.100.0.12 session state is not established; State:Idle", + "AFI:ipv4 SAFI:unicast VRF:default - Peer:10.100.0.12 AFI/SAFI state is not negotiated; Advertised:False, Received:False, Enabled:True", + "AFI:ipv4 SAFI:sr-te VRF:MGMT - Peer:10.100.0.12 session state is not established; State:Idle", + "AFI:ipv4 SAFI:sr-te VRF:MGMT - Peer:10.100.0.12 AFI/SAFI state is not negotiated; Advertised:False, Received:False, Enabled:False", + "AFI:path-selection - Peer:10.100.0.13 session state is not established; State:Idle", + "AFI:path-selection - Peer:10.100.0.13 AFI/SAFI state is not negotiated; Advertised:True, Received:False, Enabled:False", + "AFI:link-state - Peer:10.100.0.14 session state is not established; State:Idle", + "AFI:link-state - Peer:10.100.0.14 AFI/SAFI state is not negotiated; Advertised:False, Received:False, Enabled:False", ], }, }, { - "name": "success-vrf-all", + "name": "failure-tcp-queues", "test": VerifyBGPPeersHealth, "eos_data": [ { "vrfs": { "default": { - "peers": { - "10.1.255.0": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.255.2": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - }, - "PROD": { - "peers": { - "10.1.254.1": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "192.168.1.11": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + "peerList": [ + { + "peerAddress": "10.100.0.12", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 4, "inputQueueLength": 2}, }, - }, - }, - } - }, - { - "vrfs": { - "default": { - "peers": { - "10.1.255.10": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + { + "peerAddress": "10.100.0.13", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"dps": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 1, "inputQueueLength": 1}, }, - "10.1.255.12": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + { + "peerAddress": "10.100.0.14", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"linkState": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 2, "inputQueueLength": 3}, }, - }, + ] }, - "PROD": { - "peers": { - "10.1.254.11": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "192.168.1.111": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.12", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4SrTe": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 1, "inputQueueLength": 5}, }, - }, + ] }, } - }, + } ], "inputs": { "address_families": [ - {"afi": "ipv4", "safi": "unicast", "vrf": "all"}, - {"afi": "ipv4", "safi": "sr-te", "vrf": "all"}, + {"afi": "ipv4", "safi": "unicast", "vrf": "default"}, + {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT"}, + {"afi": "path-selection"}, + {"afi": "link-state"}, ] }, "expected": { - "result": "success", + "result": "failure", + "messages": [ + "AFI:ipv4 SAFI:unicast VRF:default - Peer:10.100.0.12 session state is not established; State:Idle", + "AFI:ipv4 SAFI:unicast VRF:default - Peer:10.100.0.12 session has non-empty message queues; InQ: 2, OutQ: 4", + "AFI:ipv4 SAFI:sr-te VRF:MGMT - Peer:10.100.0.12 session state is not established; State:Idle", + "AFI:ipv4 SAFI:sr-te VRF:MGMT - Peer:10.100.0.12 session has non-empty message queues; InQ: 5, OutQ: 1", + "AFI:path-selection - Peer:10.100.0.13 session state is not established; State:Idle", + "AFI:path-selection - Peer:10.100.0.13 session has non-empty message queues; InQ: 1, OutQ: 1", + "AFI:link-state - Peer:10.100.0.14 session state is not established; State:Idle", + "AFI:link-state - Peer:10.100.0.14 session has non-empty message queues; InQ: 3, OutQ: 2", + ], }, }, { - "name": "failure-issues-vrf-all", - "test": VerifyBGPPeersHealth, + "name": "success", + "test": VerifyBGPSpecificPeers, "eos_data": [ { "vrfs": { "default": { - "peers": { - "10.1.255.0": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Idle", + "peerList": [ + { + "peerAddress": "10.100.0.12", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - "10.1.255.2": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + { + "peerAddress": "10.100.0.13", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"l2VpnEvpn": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - }, + ] }, - "PROD": { - "peers": { - "10.1.254.1": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "192.168.1.11": { - "inMsgQueue": 100, - "outMsgQueue": 200, - "peerState": "Established", + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.14", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - }, + ] }, } - }, + } + ], + "inputs": { + "address_families": [ + {"afi": "ipv4", "safi": "unicast", "peers": ["10.100.0.12"]}, + {"afi": "evpn", "peers": ["10.100.0.13"]}, + {"afi": "ipv4", "safi": "unicast", "vrf": "MGMT", "peers": ["10.100.0.14"]}, + ] + }, + "expected": {"result": "success"}, + }, + { + "name": "failure-peer-not-configured", + "test": VerifyBGPSpecificPeers, + "eos_data": [ { "vrfs": { "default": { - "peers": { - "10.1.255.10": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Idle", - }, - "10.1.255.12": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, + "peerList": [ + { + "peerAddress": "10.100.0.20", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"l2VpnEvpn": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, + } + ] }, - "PROD": { - "peers": { - "10.1.254.11": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "192.168.1.111": { - "inMsgQueue": 100, - "outMsgQueue": 200, - "peerState": "Established", + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.10", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - }, + ] }, } - }, + } ], "inputs": { "address_families": [ - {"afi": "ipv4", "safi": "unicast", "vrf": "all"}, - {"afi": "ipv4", "safi": "sr-te", "vrf": "all"}, + {"afi": "ipv4", "safi": "unicast", "peers": ["10.100.0.12"]}, + {"afi": "evpn", "peers": ["10.100.0.13"]}, + {"afi": "ipv4", "safi": "unicast", "vrf": "MGMT", "peers": ["10.100.0.14"]}, ] }, "expected": { "result": "failure", "messages": [ - "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'default': {'10.1.255.0': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}, " - "'PROD': {'192.168.1.11': {'peerState': 'Established', 'inMsgQueue': 100, 'outMsgQueue': 200}}}}, " - "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'default': {'10.1.255.10': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}, " - "'PROD': {'192.168.1.111': {'peerState': 'Established', 'inMsgQueue': 100, 'outMsgQueue': 200}}}}]" + "AFI:ipv4 SAFI:unicast VRF:default - Peer:10.100.0.12 not found", + "AFI:evpn - Peer:10.100.0.13 not found", + "AFI:ipv4 SAFI:unicast VRF:MGMT - Peer:10.100.0.14 not found", ], }, }, { - "name": "failure-not-configured", - "test": VerifyBGPPeersHealth, - "eos_data": [{"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}], + "name": "failure-vrf-not-configured", + "test": VerifyBGPSpecificPeers, + "eos_data": [ + { + "vrfs": {}, + } + ], "inputs": { "address_families": [ - {"afi": "ipv4", "safi": "unicast", "vrf": "DEV"}, - {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT"}, - {"afi": "link-state"}, - {"afi": "path-selection"}, + {"afi": "ipv4", "safi": "unicast", "peers": ["10.100.0.12"]}, + {"afi": "evpn", "peers": ["10.100.0.13"]}, + {"afi": "ipv4", "safi": "unicast", "vrf": "MGMT", "peers": ["10.100.0.14"]}, ] }, "expected": { "result": "failure", "messages": [ - "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'DEV': 'Not Configured'}}, " - "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'Not Configured'}}, " - "{'afi': 'link-state', 'vrfs': {'default': 'Not Configured'}}, " - "{'afi': 'path-selection', 'vrfs': {'default': 'Not Configured'}}]" + "AFI:ipv4 SAFI:unicast VRF:default - VRF not configured", + "AFI:evpn - VRF not configured", + "AFI:ipv4 SAFI:unicast VRF:MGMT - VRF not configured", ], }, }, { - "name": "failure-no-peers", - "test": VerifyBGPPeersHealth, + "name": "failure-session-not-established", + "test": VerifyBGPSpecificPeers, "eos_data": [ { "vrfs": { "default": { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": "65120", - "peers": {}, - } - } - }, - { - "vrfs": { + "peerList": [ + { + "peerAddress": "10.100.0.12", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, + } + ] + }, "MGMT": { - "vrf": "MGMT", - "routerId": "10.1.0.3", - "asn": "65120", - "peers": {}, - } - } - }, - { - "vrfs": { - "default": { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": "65120", - "peers": {}, - } - } - }, - { - "vrfs": { - "default": { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": "65120", - "peers": {}, - } + "peerList": [ + { + "peerAddress": "10.100.0.14", + "state": "Idle", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, + }, + ] + }, } - }, + } ], "inputs": { "address_families": [ - {"afi": "ipv4", "safi": "multicast"}, - {"afi": "ipv4", "safi": "sr-te", "vrf": "MGMT"}, - {"afi": "link-state"}, - {"afi": "path-selection"}, + {"afi": "ipv4", "safi": "unicast", "peers": ["10.100.0.12"]}, + {"afi": "ipv4", "safi": "unicast", "vrf": "MGMT", "peers": ["10.100.0.14"]}, ] }, "expected": { "result": "failure", "messages": [ - "Failures: [{'afi': 'ipv4', 'safi': 'multicast', 'vrfs': {'default': 'No Peers'}}, {'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'No Peers'}}, " - "{'afi': 'link-state', 'vrfs': {'default': 'No Peers'}}, {'afi': 'path-selection', 'vrfs': {'default': 'No Peers'}}]" + "AFI:ipv4 SAFI:unicast VRF:default - Peer:10.100.0.12 session state is not established; State:Idle", + "AFI:ipv4 SAFI:unicast VRF:MGMT - Peer:10.100.0.14 session state is not established; State:Idle", ], }, }, { - "name": "success", + "name": "failure-afi-safi-not-negotiated", "test": VerifyBGPSpecificPeers, "eos_data": [ { "vrfs": { "default": { - "peers": { - "10.1.255.0": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.255.2": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - } - } - }, - { - "vrfs": { + "peerList": [ + { + "peerAddress": "10.100.0.12", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": False, "received": False, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, + } + ] + }, "MGMT": { - "peers": { - "10.1.255.10": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.255.12": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - } - } - }, - { - "vrfs": { - "default": { - "peers": { - "10.1.255.20": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.255.22": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - } - } - }, - { - "vrfs": { - "default": { - "peers": { - "10.1.255.30": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.255.32": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", + "peerList": [ + { + "peerAddress": "10.100.0.14", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": False, "received": False, "enabled": False}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - }, - } + ] + }, } - }, + } ], "inputs": { "address_families": [ - # Path selection first to make sure input to output mapping is correct. - {"afi": "path-selection", "peers": ["10.1.255.20", "10.1.255.22"]}, - { - "afi": "ipv4", - "safi": "unicast", - "vrf": "default", - "peers": ["10.1.255.0", "10.1.255.2"], - }, - { - "afi": "ipv4", - "safi": "sr-te", - "vrf": "MGMT", - "peers": ["10.1.255.10", "10.1.255.12"], - }, - {"afi": "link-state", "peers": ["10.1.255.30", "10.1.255.32"]}, + {"afi": "ipv4", "safi": "unicast", "peers": ["10.100.0.12"]}, + {"afi": "ipv4", "safi": "unicast", "vrf": "MGMT", "peers": ["10.100.0.14"]}, ] }, - "expected": {"result": "success"}, + "expected": { + "result": "failure", + "messages": [ + "AFI:ipv4 SAFI:unicast VRF:default Peer:10.100.0.12 AFI/SAFI state is not negotiated; Advertised:False, Received:False, Enabled:True", + "AFI:ipv4 SAFI:unicast VRF:MGMT Peer:10.100.0.14 AFI/SAFI state is not negotiated; Advertised:False, Received:False, Enabled:False", + ], + }, }, { - "name": "failure-issues", + "name": "failure-afi-safi-not-correct", "test": VerifyBGPSpecificPeers, "eos_data": [ { "vrfs": { "default": { - "peers": { - "10.1.255.0": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Idle", - }, - "10.1.255.2": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - } - } - }, - { - "vrfs": { + "peerList": [ + { + "peerAddress": "10.100.0.12", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"l2VpnEvpn": {"advertised": False, "received": False, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, + } + ] + }, "MGMT": { - "peers": { - "10.1.255.10": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.255.12": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Idle", - }, - }, - } - } - }, - { - "vrfs": { - "default": { - "peers": { - "10.1.255.20": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Idle", - }, - "10.1.255.22": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - }, - } - } - }, - { - "vrfs": { - "default": { - "peers": { - "10.1.255.30": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Established", - }, - "10.1.255.32": { - "inMsgQueue": 0, - "outMsgQueue": 0, - "peerState": "Idle", + "peerList": [ + { + "peerAddress": "10.100.0.14", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"l2VpnEvpn": {"advertised": False, "received": False, "enabled": False}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, }, - }, - } + ] + }, } - }, + } ], "inputs": { "address_families": [ - { - "afi": "ipv4", - "safi": "unicast", - "vrf": "default", - "peers": ["10.1.255.0", "10.1.255.2"], - }, - { - "afi": "ipv4", - "safi": "sr-te", - "vrf": "MGMT", - "peers": ["10.1.255.10", "10.1.255.12"], - }, - {"afi": "path-selection", "peers": ["10.1.255.20", "10.1.255.22"]}, - {"afi": "link-state", "peers": ["10.1.255.30", "10.1.255.32"]}, - ] - }, - "expected": { - "result": "failure", - "messages": [ - "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'default': {'10.1.255.0': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}, " - "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': {'10.1.255.12': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}, " - "{'afi': 'path-selection', 'vrfs': {'default': {'10.1.255.20': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}, " - "{'afi': 'link-state', 'vrfs': {'default': {'10.1.255.32': {'peerState': 'Idle', 'inMsgQueue': 0, 'outMsgQueue': 0}}}}]" - ], - }, - }, - { - "name": "failure-not-configured", - "test": VerifyBGPSpecificPeers, - "eos_data": [{"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}, {"vrfs": {}}], - "inputs": { - "address_families": [ - { - "afi": "ipv4", - "safi": "unicast", - "vrf": "DEV", - "peers": ["10.1.255.0"], - }, - { - "afi": "ipv4", - "safi": "sr-te", - "vrf": "MGMT", - "peers": ["10.1.255.10"], - }, - {"afi": "link-state", "peers": ["10.1.255.20"]}, - {"afi": "path-selection", "peers": ["10.1.255.30"]}, + {"afi": "ipv4", "safi": "unicast", "peers": ["10.100.0.12"]}, + {"afi": "ipv4", "safi": "unicast", "vrf": "MGMT", "peers": ["10.100.0.14"]}, ] }, "expected": { "result": "failure", "messages": [ - "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'DEV': 'Not Configured'}}, " - "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': 'Not Configured'}}, {'afi': 'link-state', 'vrfs': {'default': 'Not Configured'}}, " - "{'afi': 'path-selection', 'vrfs': {'default': 'Not Configured'}}]" + "AFI:ipv4 SAFI:unicast VRF:default Peer:10.100.0.12 AFI/SAFI state is not negotiated", + "AFI:ipv4 SAFI:unicast VRF:MGMT Peer:10.100.0.14 AFI/SAFI state is not negotiated", ], }, }, { - "name": "failure-no-peers", + "name": "failure-tcp-queues", "test": VerifyBGPSpecificPeers, "eos_data": [ { "vrfs": { "default": { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": "65120", - "peers": {}, - } - } - }, - { - "vrfs": { + "peerList": [ + { + "peerAddress": "10.100.0.12", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 3, "inputQueueLength": 3}, + } + ] + }, "MGMT": { - "vrf": "MGMT", - "routerId": "10.1.0.3", - "asn": "65120", - "peers": {}, - } - } - }, - { - "vrfs": { - "default": { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": "65120", - "peers": {}, - } - } - }, - { - "vrfs": { - "default": { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": "65120", - "peers": {}, - } + "peerList": [ + { + "peerAddress": "10.100.0.14", + "state": "Established", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 2, "inputQueueLength": 2}, + }, + ] + }, } - }, + } ], "inputs": { "address_families": [ - {"afi": "ipv4", "safi": "multicast", "peers": ["10.1.255.0"]}, - { - "afi": "ipv4", - "safi": "sr-te", - "vrf": "MGMT", - "peers": ["10.1.255.10"], - }, - {"afi": "link-state", "peers": ["10.1.255.20"]}, - {"afi": "path-selection", "peers": ["10.1.255.30"]}, + {"afi": "ipv4", "safi": "unicast", "peers": ["10.100.0.12"]}, + {"afi": "ipv4", "safi": "unicast", "vrf": "MGMT", "peers": ["10.100.0.14"]}, ] }, "expected": { "result": "failure", "messages": [ - "Failures: [{'afi': 'ipv4', 'safi': 'multicast', 'vrfs': {'default': {'10.1.255.0': {'peerNotFound': True}}}}, " - "{'afi': 'ipv4', 'safi': 'sr-te', 'vrfs': {'MGMT': {'10.1.255.10': {'peerNotFound': True}}}}, " - "{'afi': 'link-state', 'vrfs': {'default': {'10.1.255.20': {'peerNotFound': True}}}}, " - "{'afi': 'path-selection', 'vrfs': {'default': {'10.1.255.30': {'peerNotFound': True}}}}]" + "AFI:ipv4 SAFI:unicast VRF:default - Peer:10.100.0.12 session has non-empty message queues; InQ: 3, OutQ: 3", + "AFI:ipv4 SAFI:unicast VRF:MGMT - Peer:10.100.0.14 session has non-empty message queues; InQ: 2, OutQ: 2", ], }, }, diff --git a/tests/units/test_tools.py b/tests/units/test_tools.py index 16f044333..b5410ee3c 100644 --- a/tests/units/test_tools.py +++ b/tests/units/test_tools.py @@ -11,7 +11,7 @@ import pytest -from anta.tools import convert_categories, custom_division, get_dict_superset, get_failed_logs, get_item, get_value +from anta.tools import convert_categories, custom_division, format_data, get_dict_superset, get_failed_logs, get_item, get_value TEST_GET_FAILED_LOGS_DATA = [ {"id": 1, "name": "Alice", "age": 30, "email": "alice@example.com"}, @@ -513,3 +513,17 @@ def test_convert_categories(test_input: list[str], expected_raise: AbstractConte """Test convert_categories.""" with expected_raise: assert convert_categories(test_input) == expected_result + + +@pytest.mark.parametrize( + ("input_data", "expected_output"), + [ + pytest.param({"advertised": True, "received": True, "enabled": True}, "Advertised:True, Received:True, Enabled:True"), + pytest.param({"advertised": False, "received": False}, "Advertised:False, Received:False"), + pytest.param({}, ""), + pytest.param({"test": True}, "Test:True"), + ], +) +def test_format_data(input_data: dict[str, bool], expected_output: str) -> None: + """Test format_data.""" + assert format_data(input_data) == expected_output