diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index da1b4b9..7c6ef37 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6410,6 +6410,118 @@ def svccore_excessive_data_check(**kwargs): return Result(result=ERROR, msg="Error occurred while fetching svccore object counts: {}".format(str(e)), doc_url=doc_url) +@check_wrapper(check_title="Cleanup vnsRsCIfAtt usage in services") +def vns_rscifatt_cleanup_check(tversion, **kwargs): + result = PASS + headers = ["Tenant", "Device Name", "Cluster Interface", "Missing Concrete Interface", "vnsRsCIfAtt DN"] + data = [] + recommended_action = ( + "Mo vnsRsCIfAtt is deprecated >=6.0(3d). Before upgrade, under Services, add the missing concrete interface as vnsRsCIfAttN under the same cluster interface" + ) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#cleanup-vnsrscifatt-usage-in-services" + + if not tversion: + return Result(result=MANUAL, msg=TVER_MISSING, doc_url=doc_url) + + if tversion.older_than("6.0(3d)"): + return Result(result=NA, msg=VER_NOT_AFFECTED, doc_url=doc_url) + + vnsRsCIfAtts = icurl("class", "vnsRsCIfAtt.json?rsp-prop-include=config-only") + if not vnsRsCIfAtts: + return Result(result=PASS, msg="No user-configured vnsRsCIfAtt payload found.", doc_url=doc_url) + + vnsRsCIfAttNs = icurl("class", "vnsRsCIfAttN.json?rsp-prop-include=config-only") + + def get_target_dn(relation_attributes): + target_dn = relation_attributes["tDn"].strip() if "tDn" in relation_attributes else "" + if target_dn: + return target_dn + if "dn" not in relation_attributes: + return "" + relation_dn = relation_attributes["dn"] + target_dn_match = re.search(r"\[(.*)\]$", relation_dn) + return target_dn_match.group(1).strip() if target_dn_match else "" + + def get_parent_dn(relation_dn): + relation_dn = relation_dn.strip() + if "/rscIfAtt-[" in relation_dn: + return relation_dn.split("/rscIfAtt-[", 1)[0] + if "/rscIfAttN-[" in relation_dn: + return relation_dn.split("/rscIfAttN-[", 1)[0] + return relation_dn.rsplit("/", 1)[0] if "/" in relation_dn else "" + + def parse_relation_context(relation_dn): + tenant_name = "" + device_name = "" + logical_interface = "" + concrete_interface = "" + + dn_match = re.search( + r"uni/tn-(?P[^/]+)/lDevVip-(?P[^/]+)/lIf-(?P[^/]+)/" + r"rscIfAtt-\[.*?/cIf-\[(?P[^\]]+)\]\]", + relation_dn, + ) + if dn_match: + tenant_name = dn_match.group("tenant") + device_name = dn_match.group("device") + logical_interface = dn_match.group("lif") + concrete_interface = dn_match.group("cif") + return tenant_name, device_name, logical_interface, concrete_interface + + old_relation_dn_by_key = {} + for vnsRsCIfAtt in vnsRsCIfAtts: + if "vnsRsCIfAtt" not in vnsRsCIfAtt: + continue + if "attributes" not in vnsRsCIfAtt["vnsRsCIfAtt"]: + continue + relation_attributes = vnsRsCIfAtt["vnsRsCIfAtt"]["attributes"] + if "dn" not in relation_attributes: + continue + + relation_dn = relation_attributes["dn"].strip() + if not relation_dn: + continue + target_dn = get_target_dn(relation_attributes) + if not target_dn: + continue + relation_key = (get_parent_dn(relation_dn), target_dn) + old_relation_dn_by_key[relation_key] = relation_dn + + if not old_relation_dn_by_key: + return Result(result=PASS, msg="No user-configured vnsRsCIfAtt payload found.", doc_url=doc_url) + + new_relation_keys = set() + for vnsRsCIfAttN in vnsRsCIfAttNs: + if "vnsRsCIfAttN" not in vnsRsCIfAttN: + continue + if "attributes" not in vnsRsCIfAttN["vnsRsCIfAttN"]: + continue + relation_attributes = vnsRsCIfAttN["vnsRsCIfAttN"]["attributes"] + if "dn" not in relation_attributes: + continue + + relation_dn = relation_attributes["dn"].strip() + if not relation_dn: + continue + target_dn = get_target_dn(relation_attributes) + if not target_dn: + continue + relation_key = (get_parent_dn(relation_dn), target_dn) + new_relation_keys.add(relation_key) + + for relation_key in sorted(old_relation_dn_by_key.keys()): + if relation_key in new_relation_keys: + continue + missing_dn = old_relation_dn_by_key[relation_key] + tenant_name, device_name, logical_interface, concrete_interface = parse_relation_context(missing_dn) + data.append([tenant_name, device_name, logical_interface, concrete_interface, missing_dn]) + + if data: + result = FAIL_O + + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) + + # ---- Script Execution ---- @@ -6581,6 +6693,7 @@ class CheckManager: rogue_ep_coop_exception_mac_check, n9k_c9408_model_lem_count_check, inband_management_policy_misconfig_check, + vns_rscifatt_cleanup_check, ] ssh_checks = [ # General diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 82f2211..96467a4 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -203,6 +203,7 @@ Items | Defect | This Script [N9K-C9408 with more than 5 N9K-X9400-16W LEMs][d31] | CSCws82819 | :white_check_mark: | :no_entry_sign: [Multi-Pod Modular Spine Bootscript File][d32] | CSCwr66848 | :white_check_mark: | :no_entry_sign: [Inband Management Policy Misconfiguration][d33]| CSCwd40071 | :white_check_mark: | :no_entry_sign: +[Cleanup vnsRsCIfAtt usage in services][d34] | CSCwr51759 | :white_check_mark: | :no_entry_sign: [d1]: #ep-announce-compatibility [d2]: #eventmgr-db-size-defect-susceptibility @@ -237,6 +238,7 @@ Items | Defect | This Script [d31]: #n9k-c9408-with-more-than-5-n9k-x9400-16w-lems [d32]: #multi-pod-modular-spine-bootscript-file [d33]: #inband-management-policy-misconfiguration +[d34]: #cleanup-vnsrscifatt-usage-in-services ## General Check Details @@ -2797,6 +2799,19 @@ Administrators may be unable to access or operate the APIC GUI, potentially impa This check will verify the count of the `svccoreCtrlr` Managed Object and raise and alarm with the bug if object count found more than 240. Remove the content or objects of `svccoreCtrlr` or `svccoreNode`. Contact Cisco TAC or upgrade to a release containing the fix for CSCws84232 before proceeding with an upgrade. +### Cleanup vnsRsCIfAtt usage in services + +Due to [CSCwr51759][70], when targeting 6.0(3)+, having only `vnsRsCIfAtt` without the corresponding `vnsRsCIfAttN` under the same `vnsLIf` can leave service graph interface attachment in an inconsistent state. + +Impact: + +If any `vnsRsCIfAtt` relation exists without a matching `vnsRsCIfAttN` for the same concrete interface target (`tDn`), the upgrade is outage-risky and should be treated as affected. + +Suggestion: + +Before the upgrade, add the missing `vnsRsCIfAttN` relation under the same cluster interface (`vnsLIf`) with the same concrete interface target (`tDn`). + + [0]: https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script [1]: https://www.cisco.com/c/dam/en/us/td/docs/Website/datacenter/apicmatrix/index.html @@ -2867,4 +2882,5 @@ This check will verify the count of the `svccoreCtrlr` Managed Object and raise [66]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwr66848 [67]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwh80837 [68]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwd40071 -[69]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCws84232 \ No newline at end of file +[69]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCws84232 +[70]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwr51759 diff --git a/tests/checks/vns_rscifatt_cleanup_check/test_vns_rscifatt_cleanup_check.py b/tests/checks/vns_rscifatt_cleanup_check/test_vns_rscifatt_cleanup_check.py new file mode 100644 index 0000000..0b43f86 --- /dev/null +++ b/tests/checks/vns_rscifatt_cleanup_check/test_vns_rscifatt_cleanup_check.py @@ -0,0 +1,78 @@ +import os +import pytest +import importlib +from helpers.utils import read_data + +script = importlib.import_module("aci-preupgrade-validation-script") + +dir = os.path.dirname(os.path.abspath(__file__)) + +test_function = "vns_rscifatt_cleanup_check" + +# icurl queries +vnsRsCIfAtt_api = "vnsRsCIfAtt.json?rsp-prop-include=config-only" +vnsRsCIfAttN_api = "vnsRsCIfAttN.json?rsp-prop-include=config-only" + + +@pytest.mark.parametrize( + "icurl_outputs, tversion, expected_result, expected_data", + [ + # Target version missing + ( + {}, + None, + script.MANUAL, + [], + ), + # Target version is not affected (< 6.0(3d)) + ( + {}, + "6.0(2h)", + script.NA, + [], + ), + # No user-configured vnsRsCIfAtt payload + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + }, + "6.1(5e)", + script.PASS, + [], + ), + # All vnsRsCIfAtt relations have matching vnsRsCIfAttN relations + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_match.json"), + vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAttN_match.json"), + }, + "6.1(5e)", + script.PASS, + [], + ), + # One vnsRsCIfAtt relation (cons) missing in vnsRsCIfAttN + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_match.json"), + vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAttN_missing_cons.json"), + }, + "6.1(5e)", + script.FAIL_O, + [ + [ + "CSCwj49418", + "test", + "intf-cons", + "cons", + "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-cons/rscIfAtt-[uni/tn-CSCwj49418/lDevVip-test/cDev-cdev/cIf-[cons]]", + ] + ], + ), + ], +) +def test_logic(run_check, mock_icurl, tversion, expected_result, expected_data): + result = run_check( + tversion=script.AciVersion(tversion) if tversion else None, + ) + assert result.result == expected_result + assert result.data == expected_data diff --git a/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAttN_match.json b/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAttN_match.json new file mode 100644 index 0000000..2344730 --- /dev/null +++ b/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAttN_match.json @@ -0,0 +1,18 @@ +[ + { + "vnsRsCIfAttN": { + "attributes": { + "dn": "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov/rscIfAttN-[uni/tn-CSCwj49418/lDevVip-test/cDev-cdev/cIf-[prov]]", + "tDn": "uni/tn-CSCwj49418/lDevVip-test/cDev-cdev/cIf-[prov]" + } + } + }, + { + "vnsRsCIfAttN": { + "attributes": { + "dn": "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-cons/rscIfAttN-[uni/tn-CSCwj49418/lDevVip-test/cDev-cdev/cIf-[cons]]", + "tDn": "uni/tn-CSCwj49418/lDevVip-test/cDev-cdev/cIf-[cons]" + } + } + } +] diff --git a/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAttN_missing_cons.json b/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAttN_missing_cons.json new file mode 100644 index 0000000..25de6eb --- /dev/null +++ b/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAttN_missing_cons.json @@ -0,0 +1,10 @@ +[ + { + "vnsRsCIfAttN": { + "attributes": { + "dn": "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov/rscIfAttN-[uni/tn-CSCwj49418/lDevVip-test/cDev-cdev/cIf-[prov]]", + "tDn": "uni/tn-CSCwj49418/lDevVip-test/cDev-cdev/cIf-[prov]" + } + } + } +] diff --git a/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAtt_empty.json b/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAtt_empty.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAtt_empty.json @@ -0,0 +1 @@ +[] diff --git a/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAtt_match.json b/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAtt_match.json new file mode 100644 index 0000000..a9b0696 --- /dev/null +++ b/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAtt_match.json @@ -0,0 +1,18 @@ +[ + { + "vnsRsCIfAtt": { + "attributes": { + "dn": "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov/rscIfAtt-[uni/tn-CSCwj49418/lDevVip-test/cDev-cdev/cIf-[prov]]", + "tDn": "uni/tn-CSCwj49418/lDevVip-test/cDev-cdev/cIf-[prov]" + } + } + }, + { + "vnsRsCIfAtt": { + "attributes": { + "dn": "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-cons/rscIfAtt-[uni/tn-CSCwj49418/lDevVip-test/cDev-cdev/cIf-[cons]]", + "tDn": "uni/tn-CSCwj49418/lDevVip-test/cDev-cdev/cIf-[cons]" + } + } + } +]