diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index d4b5de6..b6bb5f7 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -3696,6 +3696,55 @@ def validate_32_64_bit_image_check(index, total_checks, tversion, **kwargs): print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) return result +def leaf_to_spine_redundancy_check(index, total_checks, **kwargs): + title = 'Leaf to Spine Redundancy check' + result = PASS + msg = '' + headers = [" Leaf ", " Spine ", "Message" ] + data = [] + problem = 'The Leaf Switch is connected to a Single Spine' + recommended_action = 'Connect the Leaf Switch(es) to multiple Spines for Redundancy' + doc_url = '' + print_title(title, index, total_checks) + + # icurl queries + leaf_nodes_api = 'fabricNode.json' + leaf_nodes_api += '?query-target-filter=eq(fabricNode.role,"leaf")' + spine_nodes_api = 'fabricNode.json' + spine_nodes_api += '?query-target-filter=eq(fabricNode.role,"spine")' + lldp_adj_api = 'lldpAdjEp.json' + lldp_adj_api += '?query-target-filter=wcard(lldpAdjEp.sysDesc,"topology/pod")' + + leaf_nodes = icurl('class', leaf_nodes_api) + fabricNodes = icurl('class', spine_nodes_api) + spine_nodes = [dn['fabricNode']['attributes']['name'] for dn in fabricNodes] + lldp_adj = icurl('class', lldp_adj_api) + + #Check for LLDP Adj Matching with Node DN, count Number of neighbors, break if there are more than 2 + for leaf in leaf_nodes: + if leaf['fabricNode']['attributes']['nodeType'] == 'tier-2-leaf': + continue #Skip for any tier-2 Leaf + neighbors = {} + for lldp_neighbor in lldp_adj: + if leaf['fabricNode']['attributes']['dn'] in lldp_neighbor['lldpAdjEp']['attributes']['dn']: + # Add Neighborship count based on Spine name + spine = lldp_neighbor['lldpAdjEp']['attributes']['sysName'] + if spine in spine_nodes: + neighbors[spine] = neighbors.get(spine, 0 ) + 1 + else: + continue + if len(neighbors) > 1: + # Leaf has more than 1 Spine as neighbor check passed + continue + else: + # Leaf has only 1 neighbor Check fails + data.append([leaf['fabricNode']['attributes']['name'], list(neighbors.keys())[0], problem]) + if data: + result = FAIL_O + + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + return result + if __name__ == "__main__": prints(' ==== %s%s, Script Version %s ====\n' % (ts, tz, SCRIPT_VERSION)) @@ -3722,7 +3771,7 @@ def validate_32_64_bit_image_check(index, total_checks, tversion, **kwargs): "script_version": str(SCRIPT_VERSION), "check_details": [], 'cversion': str(cversion), 'tversion': str(tversion)} checks = [ - # General Checks + #General Checks apic_version_md5_check, target_version_compatibility_check, gen1_switch_compatibility_check, @@ -3737,6 +3786,7 @@ def validate_32_64_bit_image_check(index, total_checks, tversion, **kwargs): mini_aci_6_0_2_check, post_upgrade_cb_check, validate_32_64_bit_image_check, + leaf_to_spine_redundancy_check, # Faults apic_disk_space_faults_check, diff --git a/docs/docs/validations.md b/docs/docs/validations.md index b86c8e7..c1addd7 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -34,7 +34,7 @@ Items | This Script [Mini ACI Upgrade to 6.0(2)+][g14] | :white_check_mark: | :no_entry_sign: | :no_entry_sign: [Post Upgrade CallBack Integrity][g15] | :white_check_mark: | :no_entry_sign: | :no_entry_sign: [6.0(2)+ requires 32 and 64 bit switch images][g16] | :white_check_mark: | :no_entry_sign: | :no_entry_sign: - +[Leaf to Spine Redundancy Validation][g17] | :white_check_mark: | :no_entry_sign: | :no_entry_sign: [g1]: #compatibility-target-aci-version [g2]: #compatibility-cimc-version @@ -52,6 +52,7 @@ Items | This Script [g14]: #mini-aci-upgrade-to-602-or-later [g15]: #post-upgrade-callback-integrity [g16]: #602-requires-32-and-64-bit-switch-images +[g17]: #leaf-to-spine-redundancy-validation ### Fault Checks Items | Faults | This Script | APIC built-in | Pre-Upgrade Validator (App) @@ -427,6 +428,10 @@ When targeting any version that is 6.0(2) or greater, download both the 32-bit a For additional information, see the [Guidelines and Limitations for Upgrading or Downgrading][28] section of the Cisco APIC Installation and ACI Upgrade and Downgrade Guide. +### Leaf to Spine Redundancy Validation +When Upgrading the Spine Switches Data plane traffic will be affected, any Leaf Switch connecting to that Spine will failover to another active Spine. +If a Leaf Switch is single homed to a Spine, traffic for and from the Leaf will be black holed during upgrade process. +For additional information, check [Cisco ACI Best Practices Quick Summary][31] ## Fault Check Details @@ -1971,4 +1976,5 @@ If found, the target version of your upgrade should be a version with a fix for [27]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwb91766 [28]: https://www.cisco.com/c/en/us/td/docs/dcn/aci/apic/all/apic-installation-aci-upgrade-downgrade/Cisco-APIC-Installation-ACI-Upgrade-Downgrade-Guide/m-aci-upgrade-downgrade-architecture.html#Cisco_Reference.dita_22480abb-4138-416b-8dd5-ecde23f707b4 [29]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwb86706 -[30]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwf44222 \ No newline at end of file +[30]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwf44222 +[31]: https://www.cisco.com/c/en/us/td/docs/dcn/whitepapers/cisco-aci-best-practices-quick-summary.html#SwitchConnectivity \ No newline at end of file diff --git a/tests/leaf_to_spine_redundancy_check/fabricNode-leaf.json b/tests/leaf_to_spine_redundancy_check/fabricNode-leaf.json new file mode 100644 index 0000000..39e2a1d --- /dev/null +++ b/tests/leaf_to_spine_redundancy_check/fabricNode-leaf.json @@ -0,0 +1,34 @@ +[ + { + "fabricNode": { + "attributes": { + "dn": "topology/pod-1/node-101", + "id": "101", + "name": "LF101", + "role": "leaf", + "nodeType": "unspecified" + } + } + }, + { + "fabricNode": { + "attributes": { + "dn": "topology/pod-1/node-102", + "id": "102", + "name": "LF102", + "role": "leaf", + "nodeType": "unspecified" + } + } + }, + { + "fabricNode": { + "attributes": { + "dn": "topology/pod-1/node-103", + "id": "103", + "role": "leaf", + "nodeType": "tier-2-leaf" + } + } + } +] \ No newline at end of file diff --git a/tests/leaf_to_spine_redundancy_check/fabricNode-spine.json b/tests/leaf_to_spine_redundancy_check/fabricNode-spine.json new file mode 100644 index 0000000..196e1f0 --- /dev/null +++ b/tests/leaf_to_spine_redundancy_check/fabricNode-spine.json @@ -0,0 +1,24 @@ +[ + { + "fabricNode": { + "attributes": { + "dn": "topology/pod-1/node-1001", + "id": "1001", + "name": "SP1001", + "role": "spine", + "nodeType": "unspecified" + } + } + }, + { + "fabricNode": { + "attributes": { + "dn": "topology/pod-1/node-1002", + "id": "1002", + "name": "SP1002", + "role": "spine", + "nodeType": "unspecified" + } + } + } +] \ No newline at end of file diff --git a/tests/leaf_to_spine_redundancy_check/lldpAdjEp-neg.json b/tests/leaf_to_spine_redundancy_check/lldpAdjEp-neg.json new file mode 100644 index 0000000..1f3abbe --- /dev/null +++ b/tests/leaf_to_spine_redundancy_check/lldpAdjEp-neg.json @@ -0,0 +1,42 @@ +[ + { + "lldpAdjEp": { + "attributes": { + "dn": "topology/pod-1/node-101/sys/lldp/inst/if-[eth1/49]/adj-1", + "portDesc": "topology/pod-1/paths-1002/pathep-[eth1/4]", + "sysDesc": "topology/pod-1/node-1002", + "sysName": "SP1002" + } + } + }, + { + "lldpAdjEp": { + "attributes": { + "dn": "topology/pod-1/node-101/sys/lldp/inst/if-[eth1/50]/adj-1", + "portDesc": "topology/pod-1/paths-1002/pathep-[eth1/7]", + "sysDesc": "topology/pod-1/node-1002", + "sysName": "SP1002" + } + } + }, + { + "lldpAdjEp": { + "attributes": { + "dn": "topology/pod-1/node-102/sys/lldp/inst/if-[eth1/50]/adj-1", + "portDesc": "topology/pod-1/paths-1001/pathep-[eth1/9]", + "sysDesc": "topology/pod-1/node-1001", + "sysName": "SP1001" + } + } + }, + { + "lldpAdjEp": { + "attributes": { + "dn": "topology/pod-1/node-102/sys/lldp/inst/if-[eth1/49]/adj-1", + "portDesc": "topology/pod-1/paths-1002/pathep-[eth1/10]", + "sysDesc": "topology/pod-1/node-1002", + "sysName": "SP1002" + } + } + } +] diff --git a/tests/leaf_to_spine_redundancy_check/lldpAdjEp-pos.json b/tests/leaf_to_spine_redundancy_check/lldpAdjEp-pos.json new file mode 100644 index 0000000..cfe6cd6 --- /dev/null +++ b/tests/leaf_to_spine_redundancy_check/lldpAdjEp-pos.json @@ -0,0 +1,52 @@ +[ + { + "lldpAdjEp": { + "attributes": { + "dn": "topology/pod-1/node-101/sys/lldp/inst/if-[eth1/49]/adj-1", + "portDesc": "topology/pod-1/paths-1001/pathep-[eth1/4]", + "sysDesc": "topology/pod-1/node-1001", + "sysName": "SP1001" + } + } + }, + { + "lldpAdjEp": { + "attributes": { + "dn": "topology/pod-1/node-101/sys/lldp/inst/if-[eth1/50]/adj-1", + "portDesc": "topology/pod-1/paths-1002/pathep-[eth1/7]", + "sysDesc": "topology/pod-1/node-1002", + "sysName": "SP1002" + } + } + }, + { + "lldpAdjEp": { + "attributes": { + "dn": "topology/pod-1/node-102/sys/lldp/inst/if-[eth1/50]/adj-1", + "portDesc": "topology/pod-1/paths-1001/pathep-[eth1/9]", + "sysDesc": "topology/pod-1/node-1001", + "sysName": "SP1001" + } + } + }, + { + "lldpAdjEp": { + "attributes": { + "dn": "topology/pod-1/node-102/sys/lldp/inst/if-[eth1/49]/adj-1", + "portDesc": "topology/pod-1/paths-1002/pathep-[eth1/10]", + "sysDesc": "topology/pod-1/node-1002", + "sysName": "SP1002" + } + } + }, + { + "lldpAdjEp": { + "attributes": { + "dn": "topology/pod-1/node-103/sys/lldp/inst/if-[eth1/49]/adj-1", + "portDesc": "topology/pod-1/paths-102/pathep-[eth1/10]", + "sysDesc": "topology/pod-1/node-102", + "sysName": "LF1002" + } + } + } +] diff --git a/tests/leaf_to_spine_redundancy_check/test_leaf_to_spine_redundancy_check.py b/tests/leaf_to_spine_redundancy_check/test_leaf_to_spine_redundancy_check.py new file mode 100644 index 0000000..cb0c866 --- /dev/null +++ b/tests/leaf_to_spine_redundancy_check/test_leaf_to_spine_redundancy_check.py @@ -0,0 +1,51 @@ +import os +import pytest +import logging +import importlib +from helpers.utils import read_data + +script = importlib.import_module("aci-preupgrade-validation-script") + +log = logging.getLogger(__name__) +dir = os.path.dirname(os.path.abspath(__file__)) + +# icurl queries +leaf_nodes_api = 'fabricNode.json' +leaf_nodes_api += '?query-target-filter=eq(fabricNode.role,"leaf")' + +#icurl queries +spine_nodes_api = 'fabricNode.json' +spine_nodes_api += '?query-target-filter=eq(fabricNode.role,"spine")' + +# icurl queries +lldp_adj_api = 'lldpAdjEp.json' +lldp_adj_api += '?query-target-filter=wcard(lldpAdjEp.sysDesc,"topology/pod")' + + +@pytest.mark.parametrize( + "icurl_outputs, expected_result", + [ + ##FAILING = ONE LEAF SWITCH IS SINGLE-HOMED, OTHER IS MULTI-HOMED, TIER2 LEAF IN THE NODE LIST + ( + { + leaf_nodes_api: read_data(dir, "fabricNode-leaf.json"), + lldp_adj_api: read_data(dir, "lldpAdjEp-neg.json"), + spine_nodes_api: read_data(dir, "fabricNode-spine.json") + }, + script.FAIL_O, + ), + ##PASSING = ALL LEAF SWITCHES ARE MULTI-HOMED , TIER2 LEAF IN THE NODE LIST + ( + { + leaf_nodes_api: read_data(dir, "fabricNode-leaf.json"), + lldp_adj_api: read_data(dir, "lldpAdjEp-pos.json"), + spine_nodes_api: read_data(dir, "fabricNode-spine.json") + }, + script.PASS, + ), + + ], +) +def test_logic(mock_icurl , expected_result): + result = script.leaf_to_spine_redundancy_check(1, 1 ) + assert result == expected_result \ No newline at end of file