From b892413aa107a17908e5fdec9a59cdb055d98ac7 Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Mon, 1 Jun 2026 17:45:40 +0000 Subject: [PATCH 01/19] New validation for CSCwr51759 --- aci-preupgrade-validation-script.py | 115 +++++++++++++++++- docs/docs/validations.md | 23 +++- .../test_vns_rscifatt_cleanup_check.py | 78 ++++++++++++ .../vnsRsCIfAttN_match.json | 18 +++ .../vnsRsCIfAttN_missing_cons.json | 10 ++ .../vnsRsCIfAtt_empty.json | 1 + .../vnsRsCIfAtt_match.json | 18 +++ 7 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 tests/checks/vns_rscifatt_cleanup_check/test_vns_rscifatt_cleanup_check.py create mode 100644 tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAttN_match.json create mode 100644 tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAttN_missing_cons.json create mode 100644 tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAtt_empty.json create mode 100644 tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAtt_match.json diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 5e79f56e..5d4394b4 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6699,6 +6699,119 @@ def stale_dbgacEpgSummaryTask_check(tversion, **kwargs): if data: result = FAIL_UF + + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, 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) @@ -6877,7 +6990,7 @@ class CheckManager: wred_affected_model_check, n9k_c93180yc_fx3_switch_memory_check, stale_dbgacEpgSummaryTask_check, - + vns_rscifatt_cleanup_check, ] ssh_checks = [ # General diff --git a/docs/docs/validations.md b/docs/docs/validations.md index f7886811..cf10d9da 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -206,6 +206,7 @@ Items | Defect | This Script [WRED with Affected FM Models][d35] | CSCwt50713 | :white_check_mark: | :no_entry_sign: [N9K-C93180YC-FX3 Switch Memory Less Than 32GB][d36] | CSCwm42741 | :white_check_mark: | :no_entry_sign: [Stale dbgacEpgSummaryTask Objects][d37] | CSCwt69100 | :white_check_mark: | :no_entry_sign: +[Cleanup vnsRsCIfAtt usage in services][d38] | CSCwr51759 | :white_check_mark: | :no_entry_sign: [d1]: #ep-announce-compatibility [d2]: #eventmgr-db-size-defect-susceptibility @@ -244,6 +245,7 @@ Items | Defect | This Script [d35]: #wred-with-affected-fm-models [d36]: #n9k-c93180yc-fx3-switch-memory-less-than-32gb [d37]: #stale-dbgacepgsummarytask-objects +[d38]: #cleanup-vnsrscifatt-usage-in-services ## General Check Details @@ -2783,6 +2785,25 @@ This issue happens only when the target version is specifically 6.1(4h). To avoid this issue, change the target version to another version. Or verify that the `bootscript` file exists in the bootflash of each modular spine switch prior to upgrading to 6.1(4h). If the file is missing, you have to do clean reboot on the impacted spine to ensure that `/bootflash/bootscript` gets created again. In case you already upgraded your spine and you are experiencing the traffic impact due to this issue, clean reboot of the spine will restore the traffic. +### 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: + +Upgrade can be outage-risky for service graph traffic if stale legacy-only interface attachment relations remain. + +How this check works: + +It compares configured `vnsRsCIfAtt` and `vnsRsCIfAttN` relations by the same cluster interface parent (`vnsLIf`) and same concrete interface target (`tDn`). + +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`). + + ### Inband Management Policy Misconfiguration Due to the defect [CSCwh80837][67], starting from version 6.0(4c), mgmtRsInBStNode policy get modified in leaf/spine during Apic upgrade. @@ -2923,4 +2944,4 @@ Contact Cisco TAC for next steps. For more details, refer to the workaround in [ [74]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwm42741 [75]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt69100 [76]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt38698 - +[77]: 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 00000000..0b43f86b --- /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 00000000..23447301 --- /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 00000000..25de6eb8 --- /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 00000000..fe51488c --- /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 00000000..a9b06968 --- /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]" + } + } + } +] From 371cf75f4f0492339b0ff71e931b63be081f3a08 Mon Sep 17 00:00:00 2001 From: Harinadh-Saladi Date: Mon, 1 Jun 2026 23:34:02 +0530 Subject: [PATCH 02/19] Updated validations.md --- docs/docs/validations.md | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/docs/docs/validations.md b/docs/docs/validations.md index cf10d9da..e3eb2da0 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -2785,25 +2785,6 @@ This issue happens only when the target version is specifically 6.1(4h). To avoid this issue, change the target version to another version. Or verify that the `bootscript` file exists in the bootflash of each modular spine switch prior to upgrading to 6.1(4h). If the file is missing, you have to do clean reboot on the impacted spine to ensure that `/bootflash/bootscript` gets created again. In case you already upgraded your spine and you are experiencing the traffic impact due to this issue, clean reboot of the spine will restore the traffic. -### 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: - -Upgrade can be outage-risky for service graph traffic if stale legacy-only interface attachment relations remain. - -How this check works: - -It compares configured `vnsRsCIfAtt` and `vnsRsCIfAttN` relations by the same cluster interface parent (`vnsLIf`) and same concrete interface target (`tDn`). - -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`). - - ### Inband Management Policy Misconfiguration Due to the defect [CSCwh80837][67], starting from version 6.0(4c), mgmtRsInBStNode policy get modified in leaf/spine during Apic upgrade. @@ -2831,6 +2812,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`). + + ### WRED with Affected FM Models @@ -2937,6 +2931,7 @@ Contact Cisco TAC for next steps. For more details, refer to the workaround in [ [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 +<<<<<<< HEAD [70]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCvo27498 [71]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt78235 [72]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt50713 @@ -2945,3 +2940,6 @@ Contact Cisco TAC for next steps. For more details, refer to the workaround in [ [75]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt69100 [76]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt38698 [77]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwr51759 +======= +[70]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwr51759 +>>>>>>> Updated validations.md From b21672957e9d41d8630d0f42f58d18393d86104a Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Thu, 4 Jun 2026 11:27:43 +0000 Subject: [PATCH 03/19] Addressed the review comments --- aci-preupgrade-validation-script.py | 51 +++++++------------ docs/docs/validations.md | 27 ++++++---- .../test_vns_rscifattn_missing_check.py} | 2 +- .../vnsRsCIfAttN_match.json | 0 .../vnsRsCIfAttN_missing_cons.json | 0 .../vnsRsCIfAtt_empty.json | 0 .../vnsRsCIfAtt_match.json | 0 7 files changed, 34 insertions(+), 46 deletions(-) rename tests/checks/{vns_rscifatt_cleanup_check/test_vns_rscifatt_cleanup_check.py => vns_rscifattn_missing_check/test_vns_rscifattn_missing_check.py} (97%) rename tests/checks/{vns_rscifatt_cleanup_check => vns_rscifattn_missing_check}/vnsRsCIfAttN_match.json (100%) rename tests/checks/{vns_rscifatt_cleanup_check => vns_rscifattn_missing_check}/vnsRsCIfAttN_missing_cons.json (100%) rename tests/checks/{vns_rscifatt_cleanup_check => vns_rscifattn_missing_check}/vnsRsCIfAtt_empty.json (100%) rename tests/checks/{vns_rscifatt_cleanup_check => vns_rscifattn_missing_check}/vnsRsCIfAtt_match.json (100%) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 5d4394b4..b4168dd5 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6516,6 +6516,7 @@ 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) +<<<<<<< HEAD @check_wrapper(check_title='BGP Timer Policy Already Existing (F0467 bgpProt-policy-already-existing)') def bgpProto_timer_policy_already_existing_check(tversion, cversion, **kwargs): result = FAIL_O @@ -6705,13 +6706,17 @@ def stale_dbgacEpgSummaryTask_check(tversion, **kwargs): @check_wrapper(check_title="Cleanup vnsRsCIfAtt usage in services") def vns_rscifatt_cleanup_check(tversion, **kwargs): +======= +@check_wrapper(check_title="Check missing vnsRsCIfAttN") +def vns_rscifattn_missing_check(tversion, **kwargs): +>>>>>>> Addressed the review comments 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" + "From 6.0(3) release, Mo vnsRsCIfAtt is deprecated. Before upgrade, re-add any missing concrete interface mapping under the same L4-L7 device cluster interface so the corresponding vnsRsCIfAttN relation exists." ) - doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#cleanup-vnsrscifatt-usage-in-services" + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#check-missing-vnsrscifattn" if not tversion: return Result(result=MANUAL, msg=TVER_MISSING, doc_url=doc_url) @@ -6725,23 +6730,11 @@ def vns_rscifatt_cleanup_check(tversion, **kwargs): 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): + def get_common_relation_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 "" + return relation_dn.replace("/rscIfAttN-[", "/rscIfAtt-[", 1) + return relation_dn def parse_relation_context(relation_dn): tenant_name = "" @@ -6774,10 +6767,7 @@ def parse_relation_context(relation_dn): 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) + relation_key = get_common_relation_dn(relation_dn) old_relation_dn_by_key[relation_key] = relation_dn if not old_relation_dn_by_key: @@ -6785,21 +6775,10 @@ def parse_relation_context(relation_dn): 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() + relation_dn = vnsRsCIfAttN["vnsRsCIfAttN"]["attributes"].get("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) + relation_key = get_common_relation_dn(relation_dn) new_relation_keys.add(relation_key) for relation_key in sorted(old_relation_dn_by_key.keys()): @@ -6907,6 +6886,7 @@ class CheckManager: fabric_link_redundancy_check, apic_downgrade_compat_warning_check, svccore_excessive_data_check, + vns_rscifattn_missing_check, # Faults apic_disk_space_faults_check, @@ -6986,11 +6966,14 @@ class CheckManager: rogue_ep_coop_exception_mac_check, n9k_c9408_model_lem_count_check, inband_management_policy_misconfig_check, +<<<<<<< HEAD bgpProto_timer_policy_already_existing_check, wred_affected_model_check, n9k_c93180yc_fx3_switch_memory_check, stale_dbgacEpgSummaryTask_check, vns_rscifatt_cleanup_check, +======= +>>>>>>> Addressed the review comments ] ssh_checks = [ # General diff --git a/docs/docs/validations.md b/docs/docs/validations.md index e3eb2da0..f5d4bd9c 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -39,6 +39,8 @@ Items | This Script [APIC downgrade compatibility when crossing 6.2 release][g19]| :white_check_mark: | :no_entry_sign: [Supported Hardware Compatibility][g20] | :white_check_mark: | :no_entry_sign: [Svccore Excessive Data Check][g21] | :white_check_mark: | :no_entry_sign: +[Check missing vnsRsCIfAttN][g22] | :white_check_mark: | :no_entry_sign: + [g1]: #compatibility-target-aci-version [g2]: #compatibility-cimc-version @@ -61,6 +63,7 @@ Items | This Script [g19]: #apic-downgrade-compatibility-when-crossing-62-release [g20]: #supported-hardware-compatibility [g21]: #svccore-excessive-data-check +[g22]: #check-missing-vnsrscifattn ### Fault Checks Items | Faults | This Script | APIC built-in @@ -202,11 +205,15 @@ 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: +<<<<<<< HEAD [BgpProto timer policy already existing][d34] | CSCwt78235 | :white_check_mark: | :no_entry_sign: [WRED with Affected FM Models][d35] | CSCwt50713 | :white_check_mark: | :no_entry_sign: [N9K-C93180YC-FX3 Switch Memory Less Than 32GB][d36] | CSCwm42741 | :white_check_mark: | :no_entry_sign: [Stale dbgacEpgSummaryTask Objects][d37] | CSCwt69100 | :white_check_mark: | :no_entry_sign: [Cleanup vnsRsCIfAtt usage in services][d38] | CSCwr51759 | :white_check_mark: | :no_entry_sign: +======= + +>>>>>>> Addressed the review comments [d1]: #ep-announce-compatibility [d2]: #eventmgr-db-size-defect-susceptibility @@ -241,12 +248,15 @@ 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 +<<<<<<< HEAD [d34]: #bgpProto-timer-policy-already-existing [d35]: #wred-with-affected-fm-models [d36]: #n9k-c93180yc-fx3-switch-memory-less-than-32gb [d37]: #stale-dbgacepgsummarytask-objects [d38]: #cleanup-vnsrscifatt-usage-in-services +======= +>>>>>>> Addressed the review comments ## General Check Details ### Compatibility (Target ACI Version) @@ -2784,7 +2794,6 @@ This issue happens only when the target version is specifically 6.1(4h). To avoid this issue, change the target version to another version. Or verify that the `bootscript` file exists in the bootflash of each modular spine switch prior to upgrading to 6.1(4h). If the file is missing, you have to do clean reboot on the impacted spine to ensure that `/bootflash/bootscript` gets created again. In case you already upgraded your spine and you are experiencing the traffic impact due to this issue, clean reboot of the spine will restore the traffic. - ### Inband Management Policy Misconfiguration Due to the defect [CSCwh80837][67], starting from version 6.0(4c), mgmtRsInBStNode policy get modified in leaf/spine during Apic upgrade. @@ -2812,18 +2821,11 @@ 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 +### Check missing vnsRsCIfAttN -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`). +When upgrading to 6.0(3) and above, 'vnsRsCIfAtt' get deleted and without creating the corresponding 'vnsRsCIfAttN' under the same vnsLIf will leave the service graph interface attachment in an inconsistent state +Before the upgrade, in APIC GUI navigate to Tenant > Services > L4-L7 > Device and open cluster interface `intf-prov`. If concrete interface `cons` is missing, re-add concrete interface `cons` under the same cluster interface. so the corresponding `vnsRsCIfAttN` relation exists. ### WRED with Affected FM Models @@ -2932,6 +2934,7 @@ Contact Cisco TAC for next steps. For more details, refer to the workaround in [ [68]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwd40071 [69]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCws84232 <<<<<<< HEAD +<<<<<<< HEAD [70]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCvo27498 [71]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt78235 [72]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt50713 @@ -2943,3 +2946,5 @@ Contact Cisco TAC for next steps. For more details, refer to the workaround in [ ======= [70]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwr51759 >>>>>>> Updated validations.md +======= +>>>>>>> Addressed the review comments diff --git a/tests/checks/vns_rscifatt_cleanup_check/test_vns_rscifatt_cleanup_check.py b/tests/checks/vns_rscifattn_missing_check/test_vns_rscifattn_missing_check.py similarity index 97% rename from tests/checks/vns_rscifatt_cleanup_check/test_vns_rscifatt_cleanup_check.py rename to tests/checks/vns_rscifattn_missing_check/test_vns_rscifattn_missing_check.py index 0b43f86b..53de40eb 100644 --- a/tests/checks/vns_rscifatt_cleanup_check/test_vns_rscifatt_cleanup_check.py +++ b/tests/checks/vns_rscifattn_missing_check/test_vns_rscifattn_missing_check.py @@ -7,7 +7,7 @@ dir = os.path.dirname(os.path.abspath(__file__)) -test_function = "vns_rscifatt_cleanup_check" +test_function = "vns_rscifattn_missing_check" # icurl queries vnsRsCIfAtt_api = "vnsRsCIfAtt.json?rsp-prop-include=config-only" diff --git a/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAttN_match.json b/tests/checks/vns_rscifattn_missing_check/vnsRsCIfAttN_match.json similarity index 100% rename from tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAttN_match.json rename to tests/checks/vns_rscifattn_missing_check/vnsRsCIfAttN_match.json diff --git a/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAttN_missing_cons.json b/tests/checks/vns_rscifattn_missing_check/vnsRsCIfAttN_missing_cons.json similarity index 100% rename from tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAttN_missing_cons.json rename to tests/checks/vns_rscifattn_missing_check/vnsRsCIfAttN_missing_cons.json diff --git a/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAtt_empty.json b/tests/checks/vns_rscifattn_missing_check/vnsRsCIfAtt_empty.json similarity index 100% rename from tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAtt_empty.json rename to tests/checks/vns_rscifattn_missing_check/vnsRsCIfAtt_empty.json diff --git a/tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAtt_match.json b/tests/checks/vns_rscifattn_missing_check/vnsRsCIfAtt_match.json similarity index 100% rename from tests/checks/vns_rscifatt_cleanup_check/vnsRsCIfAtt_match.json rename to tests/checks/vns_rscifattn_missing_check/vnsRsCIfAtt_match.json From 3b9654c18bd899bc2eca18bf10d2ac8c80e7f1bd Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Fri, 5 Jun 2026 03:50:16 +0000 Subject: [PATCH 04/19] Addressed the review comments --- aci-preupgrade-validation-script.py | 15 +++------------ docs/docs/validations.md | 5 +++-- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index b4168dd5..df3e73a1 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6515,8 +6515,6 @@ def svccore_excessive_data_check(**kwargs): except Exception as e: return Result(result=ERROR, msg="Error occurred while fetching svccore object counts: {}".format(str(e)), doc_url=doc_url) - -<<<<<<< HEAD @check_wrapper(check_title='BGP Timer Policy Already Existing (F0467 bgpProt-policy-already-existing)') def bgpProto_timer_policy_already_existing_check(tversion, cversion, **kwargs): result = FAIL_O @@ -6704,12 +6702,8 @@ def stale_dbgacEpgSummaryTask_check(tversion, **kwargs): return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -@check_wrapper(check_title="Cleanup vnsRsCIfAtt usage in services") -def vns_rscifatt_cleanup_check(tversion, **kwargs): -======= @check_wrapper(check_title="Check missing vnsRsCIfAttN") -def vns_rscifattn_missing_check(tversion, **kwargs): ->>>>>>> Addressed the review comments +def vnsrscifattn_missing_check(tversion, **kwargs): result = PASS headers = ["Tenant", "Device Name", "Cluster Interface", "Missing Concrete Interface", "vnsRsCIfAtt DN"] data = [] @@ -6886,7 +6880,7 @@ class CheckManager: fabric_link_redundancy_check, apic_downgrade_compat_warning_check, svccore_excessive_data_check, - vns_rscifattn_missing_check, + vnsrscifattn_missing_check, # Faults apic_disk_space_faults_check, @@ -6966,14 +6960,11 @@ class CheckManager: rogue_ep_coop_exception_mac_check, n9k_c9408_model_lem_count_check, inband_management_policy_misconfig_check, -<<<<<<< HEAD bgpProto_timer_policy_already_existing_check, wred_affected_model_check, n9k_c93180yc_fx3_switch_memory_check, stale_dbgacEpgSummaryTask_check, - vns_rscifatt_cleanup_check, -======= ->>>>>>> Addressed the review comments + vnsrscifattn_missing_check, ] ssh_checks = [ # General diff --git a/docs/docs/validations.md b/docs/docs/validations.md index f5d4bd9c..15894d0a 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -2823,10 +2823,11 @@ This check will verify the count of the `svccoreCtrlr` Managed Object and raise ### Check missing vnsRsCIfAttN -When upgrading to 6.0(3) and above, 'vnsRsCIfAtt' get deleted and without creating the corresponding 'vnsRsCIfAttN' under the same vnsLIf will leave the service graph interface attachment in an inconsistent state +When upgrading to 6.0(3) and above, 'vnsRsCIfAtt' is deleted without creating 'vnsRsCIfAttN' under 'vnsLIf'. This leaves the service graph interface attachment in an inconsistent state. -Before the upgrade, in APIC GUI navigate to Tenant > Services > L4-L7 > Device and open cluster interface `intf-prov`. If concrete interface `cons` is missing, re-add concrete interface `cons` under the same cluster interface. so the corresponding `vnsRsCIfAttN` relation exists. +For all impacted DNs in this check, reattach the Concrete interfaces associated with the cluster interface under Devices in the Services > L4-L7 tab. +Tenant --> Services --> L4-L7 --> Devices (Device_name) --> cluster interface --> Concrete interfaces ### WRED with Affected FM Models From ddde9adeb82978cf34dc0f4f183ef029fe66c21f Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Fri, 5 Jun 2026 04:03:42 +0000 Subject: [PATCH 05/19] Updated check name in all the places --- .../test_vnsrscifattn_missing_check.py} | 3 +-- .../vnsRsCIfAttN_match.json | 0 .../vnsRsCIfAttN_missing_cons.json | 0 .../vnsRsCIfAtt_empty.json | 0 .../vnsRsCIfAtt_match.json | 0 5 files changed, 1 insertion(+), 2 deletions(-) rename tests/checks/{vns_rscifattn_missing_check/test_vns_rscifattn_missing_check.py => vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py} (97%) rename tests/checks/{vns_rscifattn_missing_check => vnsrscifattn_missing_check}/vnsRsCIfAttN_match.json (100%) rename tests/checks/{vns_rscifattn_missing_check => vnsrscifattn_missing_check}/vnsRsCIfAttN_missing_cons.json (100%) rename tests/checks/{vns_rscifattn_missing_check => vnsrscifattn_missing_check}/vnsRsCIfAtt_empty.json (100%) rename tests/checks/{vns_rscifattn_missing_check => vnsrscifattn_missing_check}/vnsRsCIfAtt_match.json (100%) diff --git a/tests/checks/vns_rscifattn_missing_check/test_vns_rscifattn_missing_check.py b/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py similarity index 97% rename from tests/checks/vns_rscifattn_missing_check/test_vns_rscifattn_missing_check.py rename to tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py index 53de40eb..15fd5a8b 100644 --- a/tests/checks/vns_rscifattn_missing_check/test_vns_rscifattn_missing_check.py +++ b/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py @@ -7,13 +7,12 @@ dir = os.path.dirname(os.path.abspath(__file__)) -test_function = "vns_rscifattn_missing_check" +test_function = "vnsrscifattn_missing_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", [ diff --git a/tests/checks/vns_rscifattn_missing_check/vnsRsCIfAttN_match.json b/tests/checks/vnsrscifattn_missing_check/vnsRsCIfAttN_match.json similarity index 100% rename from tests/checks/vns_rscifattn_missing_check/vnsRsCIfAttN_match.json rename to tests/checks/vnsrscifattn_missing_check/vnsRsCIfAttN_match.json diff --git a/tests/checks/vns_rscifattn_missing_check/vnsRsCIfAttN_missing_cons.json b/tests/checks/vnsrscifattn_missing_check/vnsRsCIfAttN_missing_cons.json similarity index 100% rename from tests/checks/vns_rscifattn_missing_check/vnsRsCIfAttN_missing_cons.json rename to tests/checks/vnsrscifattn_missing_check/vnsRsCIfAttN_missing_cons.json diff --git a/tests/checks/vns_rscifattn_missing_check/vnsRsCIfAtt_empty.json b/tests/checks/vnsrscifattn_missing_check/vnsRsCIfAtt_empty.json similarity index 100% rename from tests/checks/vns_rscifattn_missing_check/vnsRsCIfAtt_empty.json rename to tests/checks/vnsrscifattn_missing_check/vnsRsCIfAtt_empty.json diff --git a/tests/checks/vns_rscifattn_missing_check/vnsRsCIfAtt_match.json b/tests/checks/vnsrscifattn_missing_check/vnsRsCIfAtt_match.json similarity index 100% rename from tests/checks/vns_rscifattn_missing_check/vnsRsCIfAtt_match.json rename to tests/checks/vnsrscifattn_missing_check/vnsRsCIfAtt_match.json From 1774d9170306ae5f6d24a6c770b306ba0f94a7b3 Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Fri, 5 Jun 2026 10:00:04 +0000 Subject: [PATCH 06/19] Addressed review comments --- aci-preupgrade-validation-script.py | 78 +++++++------------ docs/docs/validations.md | 17 ---- .../test_vnsrscifattn_missing_check.py | 25 ++++++ 3 files changed, 53 insertions(+), 67 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index df3e73a1..630ff562 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -25,6 +25,7 @@ from datetime import datetime from argparse import ArgumentParser from itertools import chain +from operator import itemgetter import threading import functools import shutil @@ -6724,63 +6725,40 @@ def vnsrscifattn_missing_check(tversion, **kwargs): vnsRsCIfAttNs = icurl("class", "vnsRsCIfAttN.json?rsp-prop-include=config-only") - def get_common_relation_dn(relation_dn): - relation_dn = relation_dn.strip() - if "/rscIfAttN-[" in relation_dn: - return relation_dn.replace("/rscIfAttN-[", "/rscIfAtt-[", 1) - return relation_dn - - 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: + new_dn_keys = set() + for new_mo in vnsRsCIfAttNs: + try: + dn = new_mo["vnsRsCIfAttN"]["attributes"]["dn"].strip() + except (KeyError, TypeError, AttributeError): continue - if "attributes" not in vnsRsCIfAtt["vnsRsCIfAtt"]: + if dn: + new_dn_keys.add(dn.replace("/rscIfAttN-[", "/rscIfAtt-[", 1)) + + for old_mo in vnsRsCIfAtts: + try: + old_dn = old_mo["vnsRsCIfAtt"]["attributes"]["dn"].strip() + except (KeyError, TypeError, AttributeError): continue - relation_attributes = vnsRsCIfAtt["vnsRsCIfAtt"]["attributes"] - if "dn" not in relation_attributes: + if not old_dn: continue - relation_dn = relation_attributes["dn"].strip() - if not relation_dn: + if old_dn.replace("/rscIfAttN-[", "/rscIfAtt-[", 1) in new_dn_keys: continue - relation_key = get_common_relation_dn(relation_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: - relation_dn = vnsRsCIfAttN["vnsRsCIfAttN"]["attributes"].get("dn", "").strip() - if not relation_dn: - continue - relation_key = get_common_relation_dn(relation_dn) - new_relation_keys.add(relation_key) + match = re.search( + r"uni/tn-(?P[^/]+)/lDevVip-(?P[^/]+)/lIf-(?P[^/]+)/" + r"rscIfAtt-\[.*?/cIf-\[(?P[^\]]+)\]\]", + old_dn, + ) + data.append([ + match.group("tenant") if match else "", + match.group("device") if match else "", + match.group("lif") if match else "", + match.group("cif") if match else "", + old_dn, + ]) - 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]) + data.sort(key=itemgetter(-1)) if data: result = FAIL_O diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 15894d0a..bb5c1741 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -41,7 +41,6 @@ Items | This Script [Svccore Excessive Data Check][g21] | :white_check_mark: | :no_entry_sign: [Check missing vnsRsCIfAttN][g22] | :white_check_mark: | :no_entry_sign: - [g1]: #compatibility-target-aci-version [g2]: #compatibility-cimc-version [g3]: #compatibility-switch-hardware @@ -205,16 +204,11 @@ 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: -<<<<<<< HEAD [BgpProto timer policy already existing][d34] | CSCwt78235 | :white_check_mark: | :no_entry_sign: [WRED with Affected FM Models][d35] | CSCwt50713 | :white_check_mark: | :no_entry_sign: [N9K-C93180YC-FX3 Switch Memory Less Than 32GB][d36] | CSCwm42741 | :white_check_mark: | :no_entry_sign: [Stale dbgacEpgSummaryTask Objects][d37] | CSCwt69100 | :white_check_mark: | :no_entry_sign: [Cleanup vnsRsCIfAtt usage in services][d38] | CSCwr51759 | :white_check_mark: | :no_entry_sign: -======= - ->>>>>>> Addressed the review comments - [d1]: #ep-announce-compatibility [d2]: #eventmgr-db-size-defect-susceptibility [d3]: #contract-port-22-defect @@ -248,15 +242,11 @@ 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 -<<<<<<< HEAD [d34]: #bgpProto-timer-policy-already-existing [d35]: #wred-with-affected-fm-models [d36]: #n9k-c93180yc-fx3-switch-memory-less-than-32gb [d37]: #stale-dbgacepgsummarytask-objects [d38]: #cleanup-vnsrscifatt-usage-in-services - -======= ->>>>>>> Addressed the review comments ## General Check Details ### Compatibility (Target ACI Version) @@ -2934,8 +2924,6 @@ Contact Cisco TAC for next steps. For more details, refer to the workaround in [ [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 -<<<<<<< HEAD -<<<<<<< HEAD [70]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCvo27498 [71]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt78235 [72]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt50713 @@ -2944,8 +2932,3 @@ Contact Cisco TAC for next steps. For more details, refer to the workaround in [ [75]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt69100 [76]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt38698 [77]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwr51759 -======= -[70]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwr51759 ->>>>>>> Updated validations.md -======= ->>>>>>> Addressed the review comments diff --git a/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py b/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py index 15fd5a8b..a601a71f 100644 --- a/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py +++ b/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py @@ -67,6 +67,31 @@ ] ], ), + # vnsRsCIfAttN is empty; all vnsRsCIfAtt relations are missing + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_match.json"), + vnsRsCIfAttN_api: [], + }, + "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]]", + ], + [ + "CSCwj49418", + "test", + "intf-prov", + "prov", + "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov/rscIfAtt-[uni/tn-CSCwj49418/lDevVip-test/cDev-cdev/cIf-[prov]]", + ], + ], + ), ], ) def test_logic(run_check, mock_icurl, tversion, expected_result, expected_data): From 643d553564a31469aed76b04553fbadebf0eb17f Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Fri, 12 Jun 2026 10:34:40 +0000 Subject: [PATCH 07/19] Addressed new comments --- aci-preupgrade-validation-script.py | 47 +++++++++++++++++-- docs/docs/validations.md | 6 +-- .../test_vnsrscifattn_missing_check.py | 33 ++++++++++++- .../vnsLIf_only.json | 18 +++++++ 4 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 tests/checks/vnsrscifattn_missing_check/vnsLIf_only.json diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 630ff562..185d55c6 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -25,7 +25,6 @@ from datetime import datetime from argparse import ArgumentParser from itertools import chain -from operator import itemgetter import threading import functools import shutil @@ -6707,9 +6706,10 @@ def stale_dbgacEpgSummaryTask_check(tversion, **kwargs): def vnsrscifattn_missing_check(tversion, **kwargs): result = PASS headers = ["Tenant", "Device Name", "Cluster Interface", "Missing Concrete Interface", "vnsRsCIfAtt DN"] + manual_headers = ["Tenant", "Device Name", "Cluster Interface", "Missing Concrete Interface", "vnsLIf DN"] data = [] recommended_action = ( - "From 6.0(3) release, Mo vnsRsCIfAtt is deprecated. Before upgrade, re-add any missing concrete interface mapping under the same L4-L7 device cluster interface so the corresponding vnsRsCIfAttN relation exists." + "From 6.0(3) release, Mo vnsRsCIfAtt is deprecated. Before upgrade, reattach any missing concrete interface mapping under the same L4-L7 device cluster interface so the corresponding vnsRsCIfAttN relation exists." ) doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#check-missing-vnsrscifattn" @@ -6720,11 +6720,48 @@ def vnsrscifattn_missing_check(tversion, **kwargs): return Result(result=NA, msg=VER_NOT_AFFECTED, doc_url=doc_url) vnsRsCIfAtts = icurl("class", "vnsRsCIfAtt.json?rsp-prop-include=config-only") + vnsRsCIfAttNs = icurl("class", "vnsRsCIfAttN.json?rsp-prop-include=config-only") + + if not vnsRsCIfAtts and not vnsRsCIfAttNs: + vnsLIfs = icurl("class", "vnsLIf.json?rsp-prop-include=config-only") + for vnsLIf in vnsLIfs: + try: + lif_dn = vnsLIf["vnsLIf"]["attributes"]["dn"].strip() + except (KeyError, TypeError, AttributeError): + continue + if not lif_dn: + continue + + match = re.search( + r"uni/tn-(?P[^/]+)/lDevVip-(?P[^/]+)/lIf-(?P[^/]+)$", + lif_dn, + ) + data.append([ + match.group("tenant") if match else "", + match.group("device") if match else "", + match.group("lif") if match else "", + "N/A", + lif_dn, + ]) + + data.sort(key=lambda row: row[-1]) + + if data: + msg = "vnsLIf has neither vnsRsCIfAtt nor vnsRsCIfAttN. Manual verification required to avoid service graph inconsistency." + manual_action = ( + "Under each impacted L4-L7 device cluster interface, manually verify and reattach concrete interfaces as needed." + ) + return Result(result=MANUAL, msg=msg, headers=manual_headers, data=data, recommended_action=manual_action, doc_url=doc_url) + + msg = "Both vnsRsCIfAtt and vnsRsCIfAttN are missing. Manual verification required for service graph interface attachments." + manual_action = ( + "Validate L4-L7 device cluster interfaces and reattach concrete interfaces where needed." + ) + return Result(result=MANUAL, msg=msg, recommended_action=manual_action, doc_url=doc_url) + 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") - new_dn_keys = set() for new_mo in vnsRsCIfAttNs: try: @@ -6758,7 +6795,7 @@ def vnsrscifattn_missing_check(tversion, **kwargs): old_dn, ]) - data.sort(key=itemgetter(-1)) + data.sort(key=lambda row: row[-1]) if data: result = FAIL_O diff --git a/docs/docs/validations.md b/docs/docs/validations.md index bb5c1741..d5aef3d0 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -242,11 +242,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]: #bgpProto-timer-policy-already-existing -[d35]: #wred-with-affected-fm-models -[d36]: #n9k-c93180yc-fx3-switch-memory-less-than-32gb -[d37]: #stale-dbgacepgsummarytask-objects -[d38]: #cleanup-vnsrscifatt-usage-in-services + ## General Check Details ### Compatibility (Target ACI Version) diff --git a/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py b/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py index a601a71f..42014221 100644 --- a/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py +++ b/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py @@ -12,6 +12,7 @@ # icurl queries vnsRsCIfAtt_api = "vnsRsCIfAtt.json?rsp-prop-include=config-only" vnsRsCIfAttN_api = "vnsRsCIfAttN.json?rsp-prop-include=config-only" +vnsLIf_api = "vnsLIf.json?rsp-prop-include=config-only" @pytest.mark.parametrize( "icurl_outputs, tversion, expected_result, expected_data", @@ -30,15 +31,43 @@ script.NA, [], ), - # No user-configured vnsRsCIfAtt payload + # Both vnsRsCIfAtt and vnsRsCIfAttN are missing (no vnsLIf rows) ( { vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsLIf_api: [], }, "6.1(5e)", - script.PASS, + script.MANUAL, [], ), + # Both vnsRsCIfAtt and vnsRsCIfAttN are missing while vnsLIf exists + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsLIf_api: read_data(dir, "vnsLIf_only.json"), + }, + "6.1(5e)", + script.MANUAL, + [ + [ + "CSCwj49418", + "test", + "intf-cons", + "N/A", + "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-cons", + ], + [ + "CSCwj49418", + "test", + "intf-prov", + "N/A", + "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov", + ], + ], + ), # All vnsRsCIfAtt relations have matching vnsRsCIfAttN relations ( { diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLIf_only.json b/tests/checks/vnsrscifattn_missing_check/vnsLIf_only.json new file mode 100644 index 00000000..0eb80a89 --- /dev/null +++ b/tests/checks/vnsrscifattn_missing_check/vnsLIf_only.json @@ -0,0 +1,18 @@ +[ + { + "vnsLIf": { + "attributes": { + "dn": "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-cons", + "name": "intf-cons" + } + } + }, + { + "vnsLIf": { + "attributes": { + "dn": "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov", + "name": "intf-prov" + } + } + } +] From 2f9033d917e8fc8e222ab83650ecf58394ee69bd Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Wed, 24 Jun 2026 14:47:14 +0000 Subject: [PATCH 08/19] Added new scenarios with respect to service graph missing interface detection --- aci-preupgrade-validation-script.py | 172 +++++++++++++- .../test_vnsrscifattn_missing_check.py | 218 +++++++++++++++++- .../vnsGraphInst_applied_single.json | 11 + ...DevCtx_vnsLDevIfLIf_missing_rscifattn.json | 30 +++ .../vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json | 29 +++ .../vnsLDevCtx_vnsLIf_missing_rscifattn.json | 30 +++ .../vnsLIf_common_device_both.json | 16 ++ .../vnsLIf_subtree_parent_only.json | 9 + 8 files changed, 507 insertions(+), 8 deletions(-) create mode 100644 tests/checks/vnsrscifattn_missing_check/vnsGraphInst_applied_single.json create mode 100644 tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_rscifattn.json create mode 100644 tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json create mode 100644 tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLIf_missing_rscifattn.json create mode 100644 tests/checks/vnsrscifattn_missing_check/vnsLIf_common_device_both.json create mode 100644 tests/checks/vnsrscifattn_missing_check/vnsLIf_subtree_parent_only.json diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 185d55c6..a1c862a4 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6713,6 +6713,12 @@ def vnsrscifattn_missing_check(tversion, **kwargs): ) doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#check-missing-vnsrscifattn" + lif_dn_pattern = re.compile(r"uni/tn-(?P[^/]+)/lDevVip-(?P[^/]+)/lIf-(?P[^/]+)$") + ldeviflif_pattern = re.compile( + r"^uni/tn-[^/]+/lDevIf-\[(?Puni/tn-[^\]]+/lDevVip-[^\]]+)\]/lDevIfLIf-(?P[^/]+)$" + ) + tenant_pattern = re.compile(r"^uni/tn-([^/]+)/") + if not tversion: return Result(result=MANUAL, msg=TVER_MISSING, doc_url=doc_url) @@ -6722,6 +6728,157 @@ def vnsrscifattn_missing_check(tversion, **kwargs): vnsRsCIfAtts = icurl("class", "vnsRsCIfAtt.json?rsp-prop-include=config-only") vnsRsCIfAttNs = icurl("class", "vnsRsCIfAttN.json?rsp-prop-include=config-only") + graph_query = ( + "vnsGraphInst.json?" + "query-target-filter=eq(vnsGraphInst.configSt,\"applied\")" + ) + graph_insts = icurl("class", graph_query) or [] + graph_pairs = set() + graph_label_pairs = [] + for graph_entry in graph_insts: + try: + ctrct_dn = graph_entry["vnsGraphInst"]["attributes"]["ctrctDn"].strip() + graph_dn = graph_entry["vnsGraphInst"]["attributes"]["graphDn"].strip() + except (KeyError, TypeError, AttributeError): + continue + if not ctrct_dn or not graph_dn: + continue + + contract_match = re.search(r"/brc-([^/]+)$", ctrct_dn) + graph_match = re.search(r"/AbsGraph-([^/]+)$", graph_dn) + tenant_match = tenant_pattern.search(ctrct_dn) + if not contract_match or not graph_match: + continue + + graph_pairs.add(( + tenant_match.group(1) if tenant_match else "", + contract_match.group(1), + graph_match.group(1), + )) + + for _, pair_contract, pair_graph in graph_pairs: + pair_graph_norm = pair_graph[:-9] if pair_graph.endswith("-imported") else pair_graph + graph_label_pairs.append((pair_contract, pair_graph_norm)) + + lif_dns_to_check = set() + ldev_ctx_query = ( + "vnsLDevCtx.json?" + "rsp-prop-include=config-only" + "&rsp-subtree=full" + "&rsp-subtree-class=vnsLIfCtx,vnsRsLIfCtxToLIf" + "&rsp-subtree-include=required" + ) + ldev_ctxs = icurl("class", ldev_ctx_query) or [] + for ldev_ctx_entry in ldev_ctxs: + try: + ldev_ctx_mo = ldev_ctx_entry["vnsLDevCtx"] + attrs = ldev_ctx_mo["attributes"] + ldev_ctx_dn = attrs["dn"].strip() + contract_name = attrs["ctrctNameOrLbl"].strip() + graph_name = attrs["graphNameOrLbl"].strip() + except (KeyError, TypeError, AttributeError): + continue + + if graph_pairs: + graph_name_norm = graph_name[:-9] if graph_name.endswith("-imported") else graph_name + matched = False + for pair_contract, pair_graph in graph_label_pairs: + contract_ok = ( + contract_name == pair_contract + or contract_name.endswith(pair_contract) + or pair_contract.endswith(contract_name) + ) + graph_ok = ( + graph_name == pair_graph + or graph_name_norm == pair_graph + or pair_graph.endswith(graph_name_norm) + or graph_name_norm.endswith(pair_graph) + ) + if contract_ok and graph_ok: + matched = True + break + if not matched: + continue + elif not contract_name or not graph_name: + # Fallback mode (no applied vnsGraphInst): skip incomplete contexts. + continue + + stack = [ldev_ctx_mo] + while stack: + current_mo = stack.pop() + for child in current_mo.get("children", []) or []: + lifctx_mo = child.get("vnsLIfCtx") + if lifctx_mo: + stack.append(lifctx_mo) + + rs_lifctx_to_lif = child.get("vnsRsLIfCtxToLIf") + if not rs_lifctx_to_lif: + continue + + try: + tcl = rs_lifctx_to_lif["attributes"].get("tCl", "") + tdn = rs_lifctx_to_lif["attributes"]["tDn"].strip() + except (KeyError, TypeError, AttributeError): + continue + if not tdn: + continue + + if tcl == "vnsLIf" or lif_dn_pattern.search(tdn): + lif_dns_to_check.add(tdn) + elif tcl == "vnsLDevIfLIf" or ("/lDevIf-[" in tdn and "/lDevIfLIf-" in tdn): + converted_match = ldeviflif_pattern.search(tdn) + if converted_match: + lif_dns_to_check.add("{}/lIf-{}".format(converted_match.group("base"), converted_match.group("lif"))) + + # Expand candidates to all cluster interfaces under discovered device clusters. + lif_device_prefixes = set() + for lif_dn in lif_dns_to_check: + match = lif_dn_pattern.search(lif_dn) + if not match: + continue + lif_device_prefixes.add("uni/tn-{}/lDevVip-{}".format(match.group("tenant"), match.group("device"))) + + if lif_device_prefixes: + vnsLIfs = icurl("class", "vnsLIf.json?rsp-prop-include=config-only") or [] + prefix_filters = tuple("{}/lIf-".format(prefix) for prefix in lif_device_prefixes) + for vnsLIf in vnsLIfs: + try: + lif_dn = vnsLIf["vnsLIf"]["attributes"]["dn"].strip() + except (KeyError, TypeError, AttributeError): + continue + if lif_dn and lif_dn.startswith(prefix_filters): + lif_dns_to_check.add(lif_dn) + + for lif_dn in sorted(lif_dns_to_check): + lif_query = ( + "{}.json?query-target=subtree" + "&target-subtree-class=vnsRsCIfAttN" + "&rsp-prop-include=config-only" + ).format(lif_dn) + lif_subtree = icurl("mo", lif_query) or [] + has_rscifattn = any(isinstance(mo, dict) and mo.get("vnsRsCIfAttN") for mo in lif_subtree) + if has_rscifattn: + continue + + match = lif_dn_pattern.search(lif_dn) + missing_cif = "" + if match: + lif_name = match.group("lif") + missing_cif = lif_name.rsplit("-", 1)[-1] if "-" in lif_name else lif_name + data.append([ + match.group("tenant") if match else "", + match.group("device") if match else "", + match.group("lif") if match else "", + missing_cif, + lif_dn, + ]) + + if data: + data.sort(key=lambda row: row[-1]) + if vnsRsCIfAtts or vnsRsCIfAttNs: + return Result(result=FAIL_O, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) + data = [] + if not vnsRsCIfAtts and not vnsRsCIfAttNs: vnsLIfs = icurl("class", "vnsLIf.json?rsp-prop-include=config-only") for vnsLIf in vnsLIfs: @@ -6732,10 +6889,7 @@ def vnsrscifattn_missing_check(tversion, **kwargs): if not lif_dn: continue - match = re.search( - r"uni/tn-(?P[^/]+)/lDevVip-(?P[^/]+)/lIf-(?P[^/]+)$", - lif_dn, - ) + match = lif_dn_pattern.search(lif_dn) data.append([ match.group("tenant") if match else "", match.group("device") if match else "", @@ -6745,13 +6899,19 @@ def vnsrscifattn_missing_check(tversion, **kwargs): ]) data.sort(key=lambda row: row[-1]) - if data: msg = "vnsLIf has neither vnsRsCIfAtt nor vnsRsCIfAttN. Manual verification required to avoid service graph inconsistency." manual_action = ( "Under each impacted L4-L7 device cluster interface, manually verify and reattach concrete interfaces as needed." ) - return Result(result=MANUAL, msg=msg, headers=manual_headers, data=data, recommended_action=manual_action, doc_url=doc_url) + return Result( + result=MANUAL, + msg=msg, + headers=manual_headers, + data=data, + recommended_action=manual_action, + doc_url=doc_url, + ) msg = "Both vnsRsCIfAtt and vnsRsCIfAttN are missing. Manual verification required for service graph interface attachments." manual_action = ( diff --git a/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py b/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py index 42014221..24121285 100644 --- a/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py +++ b/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py @@ -13,30 +13,76 @@ vnsRsCIfAtt_api = "vnsRsCIfAtt.json?rsp-prop-include=config-only" vnsRsCIfAttN_api = "vnsRsCIfAttN.json?rsp-prop-include=config-only" vnsLIf_api = "vnsLIf.json?rsp-prop-include=config-only" +vnsGraphInst_api = ( + 'vnsGraphInst.json?query-target-filter=eq(vnsGraphInst.configSt,"applied")' +) +vnsLDevCtx_all_api = ( + 'vnsLDevCtx.json?' + 'rsp-prop-include=config-only' + '&rsp-subtree=full' + '&rsp-subtree-class=vnsLIfCtx,vnsRsLIfCtxToLIf' + '&rsp-subtree-include=required' +) +vnsLIf_intf_prov_subtree_api = ( + 'uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov.json?query-target=subtree' + '&target-subtree-class=vnsRsCIfAttN' + '&rsp-prop-include=config-only' +) +vnsLIf_common_cons_subtree_api = ( + 'uni/tn-common/lDevVip-common-device/lIf-common-cons.json?query-target=subtree' + '&target-subtree-class=vnsRsCIfAttN' + '&rsp-prop-include=config-only' +) +vnsLIf_common_prov_subtree_api = ( + 'uni/tn-common/lDevVip-common-device/lIf-common-prov.json?query-target=subtree' + '&target-subtree-class=vnsRsCIfAttN' + '&rsp-prop-include=config-only' +) @pytest.mark.parametrize( "icurl_outputs, tversion, expected_result, expected_data", [ # Target version missing ( - {}, + { + vnsGraphInst_api: [], + vnsLDevCtx_all_api: [], + }, None, script.MANUAL, [], ), # Target version is not affected (< 6.0(3d)) ( - {}, + { + vnsGraphInst_api: [], + vnsLDevCtx_all_api: [], + }, "6.0(2h)", script.NA, [], ), + # 6.0(3d) is affected and should not return N/A + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsLIf_api: [], + vnsGraphInst_api: [], + vnsLDevCtx_all_api: [], + }, + "6.0(3d)", + script.MANUAL, + [], + ), # Both vnsRsCIfAtt and vnsRsCIfAttN are missing (no vnsLIf rows) ( { vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAtt_empty.json"), vnsLIf_api: [], + vnsGraphInst_api: [], + vnsLDevCtx_all_api: [], }, "6.1(5e)", script.MANUAL, @@ -48,6 +94,8 @@ vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAtt_empty.json"), vnsLIf_api: read_data(dir, "vnsLIf_only.json"), + vnsGraphInst_api: [], + vnsLDevCtx_all_api: [], }, "6.1(5e)", script.MANUAL, @@ -68,11 +116,25 @@ ], ], ), + # Legacy behavior: when vnsRsCIfAtt is absent but vnsRsCIfAttN exists, return PASS + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAttN_match.json"), + vnsGraphInst_api: [], + vnsLDevCtx_all_api: [], + }, + "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"), + vnsGraphInst_api: [], + vnsLDevCtx_all_api: [], }, "6.1(5e)", script.PASS, @@ -83,6 +145,8 @@ { vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_match.json"), vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAttN_missing_cons.json"), + vnsGraphInst_api: [], + vnsLDevCtx_all_api: [], }, "6.1(5e)", script.FAIL_O, @@ -101,6 +165,8 @@ { vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_match.json"), vnsRsCIfAttN_api: [], + vnsGraphInst_api: [], + vnsLDevCtx_all_api: [], }, "6.1(5e)", script.FAIL_O, @@ -121,6 +187,154 @@ ], ], ), + # vnsLIf target from vnsLIfCtx relation has no vnsRsCIfAttN subtree + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAttN_match.json"), + vnsLIf_api: [], + vnsGraphInst_api: read_data(dir, "vnsGraphInst_applied_single.json"), + vnsLDevCtx_all_api: read_data(dir, "vnsLDevCtx_vnsLIf_missing_rscifattn.json"), + vnsLIf_intf_prov_subtree_api: [], + }, + "6.1(5e)", + script.FAIL_O, + [ + [ + "CSCwj49418", + "test", + "intf-prov", + "prov", + "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov", + ] + ], + ), + # vnsLDevIfLIf target must be converted to vnsLIf DN before subtree check + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAttN_match.json"), + vnsLIf_api: [], + vnsGraphInst_api: read_data(dir, "vnsGraphInst_applied_single.json"), + vnsLDevCtx_all_api: read_data(dir, "vnsLDevCtx_vnsLDevIfLIf_missing_rscifattn.json"), + vnsLIf_common_cons_subtree_api: [], + }, + "6.1(5e)", + script.FAIL_O, + [ + [ + "common", + "common-device", + "common-cons", + "cons", + "uni/tn-common/lDevVip-common-device/lIf-common-cons", + ] + ], + ), + # Subtree may return parent vnsLIf object without vnsRsCIfAttN; still fail + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAttN_match.json"), + vnsLIf_api: [], + vnsGraphInst_api: read_data(dir, "vnsGraphInst_applied_single.json"), + vnsLDevCtx_all_api: read_data(dir, "vnsLDevCtx_vnsLIf_missing_rscifattn.json"), + vnsLIf_intf_prov_subtree_api: read_data(dir, "vnsLIf_subtree_parent_only.json"), + }, + "6.1(5e)", + script.FAIL_O, + [ + [ + "CSCwj49418", + "test", + "intf-prov", + "prov", + "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov", + ] + ], + ), + # vnsRsLIfCtxToLIf may omit tCl; infer LDevIfLIf from tDn pattern and still fail when missing + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAttN_match.json"), + vnsLIf_api: [], + vnsGraphInst_api: read_data(dir, "vnsGraphInst_applied_single.json"), + vnsLDevCtx_all_api: read_data(dir, "vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json"), + vnsLIf_common_cons_subtree_api: [], + }, + "6.1(5e)", + script.FAIL_O, + [ + [ + "common", + "common-device", + "common-cons", + "cons", + "uni/tn-common/lDevVip-common-device/lIf-common-cons", + ] + ], + ), + # If only one common interface is referenced in vnsLDevCtx, expand to all LIfs under the same device + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAttN_match.json"), + vnsLIf_api: read_data(dir, "vnsLIf_common_device_both.json"), + vnsGraphInst_api: read_data(dir, "vnsGraphInst_applied_single.json"), + vnsLDevCtx_all_api: read_data(dir, "vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json"), + vnsLIf_common_cons_subtree_api: [], + vnsLIf_common_prov_subtree_api: [], + }, + "6.1(5e)", + script.FAIL_O, + [ + [ + "common", + "common-device", + "common-cons", + "cons", + "uni/tn-common/lDevVip-common-device/lIf-common-cons", + ], + [ + "common", + "common-device", + "common-prov", + "prov", + "uni/tn-common/lDevVip-common-device/lIf-common-prov", + ], + ], + ), + # If both vnsRsCIfAtt and vnsRsCIfAttN are globally empty, result should be MANUAL + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsLIf_api: read_data(dir, "vnsLIf_common_device_both.json"), + vnsGraphInst_api: read_data(dir, "vnsGraphInst_applied_single.json"), + vnsLDevCtx_all_api: read_data(dir, "vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json"), + vnsLIf_common_cons_subtree_api: [], + vnsLIf_common_prov_subtree_api: [], + }, + "6.1(5e)", + script.MANUAL, + [ + [ + "common", + "common-device", + "common-cons", + "N/A", + "uni/tn-common/lDevVip-common-device/lIf-common-cons", + ], + [ + "common", + "common-device", + "common-prov", + "N/A", + "uni/tn-common/lDevVip-common-device/lIf-common-prov", + ], + ], + ), ], ) def test_logic(run_check, mock_icurl, tversion, expected_result, expected_data): diff --git a/tests/checks/vnsrscifattn_missing_check/vnsGraphInst_applied_single.json b/tests/checks/vnsrscifattn_missing_check/vnsGraphInst_applied_single.json new file mode 100644 index 00000000..80fe130e --- /dev/null +++ b/tests/checks/vnsrscifattn_missing_check/vnsGraphInst_applied_single.json @@ -0,0 +1,11 @@ +[ + { + "vnsGraphInst": { + "attributes": { + "configSt": "applied", + "ctrctDn": "uni/tn-CSCwj49418/brc-epg-epg", + "graphDn": "uni/tn-CSCwj49418/AbsGraph-test" + } + } + } +] diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_rscifattn.json b/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_rscifattn.json new file mode 100644 index 00000000..691fd7e3 --- /dev/null +++ b/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_rscifattn.json @@ -0,0 +1,30 @@ +[ + { + "vnsLDevCtx": { + "attributes": { + "dn": "uni/tn-CSCwj49418/ldevCtx-c-epg-epg-g-test-n-test-node", + "ctrctNameOrLbl": "epg-epg", + "graphNameOrLbl": "test" + }, + "children": [ + { + "vnsLIfCtx": { + "attributes": { + "dn": "uni/tn-CSCwj49418/ldevCtx-c-epg-epg-g-test-n-test-node/lifCtx-2" + }, + "children": [ + { + "vnsRsLIfCtxToLIf": { + "attributes": { + "tCl": "vnsLDevIfLIf", + "tDn": "uni/tn-user/lDevIf-[uni/tn-common/lDevVip-common-device]/lDevIfLIf-common-cons" + } + } + } + ] + } + } + ] + } + } +] diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json b/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json new file mode 100644 index 00000000..f42acc55 --- /dev/null +++ b/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json @@ -0,0 +1,29 @@ +[ + { + "vnsLDevCtx": { + "attributes": { + "dn": "uni/tn-user/ldevCtx-c-common-epg-epg-g-test-imported-n-N1", + "ctrctNameOrLbl": "common-epg-epg", + "graphNameOrLbl": "test-imported" + }, + "children": [ + { + "vnsLIfCtx": { + "attributes": { + "dn": "uni/tn-user/ldevCtx-c-common-epg-epg-g-test-imported-n-N1/lifCtx-cons" + }, + "children": [ + { + "vnsRsLIfCtxToLIf": { + "attributes": { + "tDn": "uni/tn-user/lDevIf-[uni/tn-common/lDevVip-common-device]/lDevIfLIf-common-cons" + } + } + } + ] + } + } + ] + } + } +] diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLIf_missing_rscifattn.json b/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLIf_missing_rscifattn.json new file mode 100644 index 00000000..96183d33 --- /dev/null +++ b/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLIf_missing_rscifattn.json @@ -0,0 +1,30 @@ +[ + { + "vnsLDevCtx": { + "attributes": { + "dn": "uni/tn-CSCwj49418/ldevCtx-c-epg-epg-g-test-n-test-node", + "ctrctNameOrLbl": "epg-epg", + "graphNameOrLbl": "test" + }, + "children": [ + { + "vnsLIfCtx": { + "attributes": { + "dn": "uni/tn-CSCwj49418/ldevCtx-c-epg-epg-g-test-n-test-node/lifCtx-1" + }, + "children": [ + { + "vnsRsLIfCtxToLIf": { + "attributes": { + "tCl": "vnsLIf", + "tDn": "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov" + } + } + } + ] + } + } + ] + } + } +] diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLIf_common_device_both.json b/tests/checks/vnsrscifattn_missing_check/vnsLIf_common_device_both.json new file mode 100644 index 00000000..8944e08e --- /dev/null +++ b/tests/checks/vnsrscifattn_missing_check/vnsLIf_common_device_both.json @@ -0,0 +1,16 @@ +[ + { + "vnsLIf": { + "attributes": { + "dn": "uni/tn-common/lDevVip-common-device/lIf-common-cons" + } + } + }, + { + "vnsLIf": { + "attributes": { + "dn": "uni/tn-common/lDevVip-common-device/lIf-common-prov" + } + } + } +] diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLIf_subtree_parent_only.json b/tests/checks/vnsrscifattn_missing_check/vnsLIf_subtree_parent_only.json new file mode 100644 index 00000000..65b75316 --- /dev/null +++ b/tests/checks/vnsrscifattn_missing_check/vnsLIf_subtree_parent_only.json @@ -0,0 +1,9 @@ +[ + { + "vnsLIf": { + "attributes": { + "dn": "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov" + } + } + } +] From be39aa32975d2a5911ab9b54af02383edae100e7 Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Sat, 27 Jun 2026 00:14:54 +0000 Subject: [PATCH 09/19] Addressed review comments --- aci-preupgrade-validation-script.py | 157 +++++++++--------- .../test_vnsrscifattn_missing_check.py | 102 ++++++------ .../vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json | 6 +- ...Ctx_vnsLDevIfLIf_missing_tcl_imported.json | 45 +++++ 4 files changed, 176 insertions(+), 134 deletions(-) create mode 100644 tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl_imported.json diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index a1c862a4..95e5c3ed 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6706,10 +6706,9 @@ def stale_dbgacEpgSummaryTask_check(tversion, **kwargs): def vnsrscifattn_missing_check(tversion, **kwargs): result = PASS headers = ["Tenant", "Device Name", "Cluster Interface", "Missing Concrete Interface", "vnsRsCIfAtt DN"] - manual_headers = ["Tenant", "Device Name", "Cluster Interface", "Missing Concrete Interface", "vnsLIf DN"] data = [] recommended_action = ( - "From 6.0(3) release, Mo vnsRsCIfAtt is deprecated. Before upgrade, reattach any missing concrete interface mapping under the same L4-L7 device cluster interface so the corresponding vnsRsCIfAttN relation exists." + "From 6.0(3) release, Object vnsRsCIfAtt is deprecated. Before upgrade, reattach any missing concrete interface mapping under the same L4-L7 device cluster interface so the corresponding vnsRsCIfAttN relation exists." ) doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#check-missing-vnsrscifattn" @@ -6725,8 +6724,13 @@ def vnsrscifattn_missing_check(tversion, **kwargs): 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") - vnsRsCIfAttNs = icurl("class", "vnsRsCIfAttN.json?rsp-prop-include=config-only") + vnsLIfs = None + + def get_vnsLIfs(): + nonlocal vnsLIfs + if vnsLIfs is None: + vnsLIfs = icurl("class", "vnsLIf.json?rsp-prop-include=config-only") or [] + return vnsLIfs graph_query = ( "vnsGraphInst.json?" @@ -6734,7 +6738,7 @@ def vnsrscifattn_missing_check(tversion, **kwargs): ) graph_insts = icurl("class", graph_query) or [] graph_pairs = set() - graph_label_pairs = [] + graph_keys = set() for graph_entry in graph_insts: try: ctrct_dn = graph_entry["vnsGraphInst"]["attributes"]["ctrctDn"].strip() @@ -6755,12 +6759,13 @@ def vnsrscifattn_missing_check(tversion, **kwargs): contract_match.group(1), graph_match.group(1), )) - - for _, pair_contract, pair_graph in graph_pairs: - pair_graph_norm = pair_graph[:-9] if pair_graph.endswith("-imported") else pair_graph - graph_label_pairs.append((pair_contract, pair_graph_norm)) + graph_keys.add(( + contract_match.group(1), + graph_match.group(1), + )) lif_dns_to_check = set() + lif_device_prefixes = set() ldev_ctx_query = ( "vnsLDevCtx.json?" "rsp-prop-include=config-only" @@ -6780,24 +6785,13 @@ def vnsrscifattn_missing_check(tversion, **kwargs): continue if graph_pairs: + contract_name_norm = contract_name.split("-", 1)[-1] if "-" in contract_name else contract_name graph_name_norm = graph_name[:-9] if graph_name.endswith("-imported") else graph_name - matched = False - for pair_contract, pair_graph in graph_label_pairs: - contract_ok = ( - contract_name == pair_contract - or contract_name.endswith(pair_contract) - or pair_contract.endswith(contract_name) - ) - graph_ok = ( - graph_name == pair_graph - or graph_name_norm == pair_graph - or pair_graph.endswith(graph_name_norm) - or graph_name_norm.endswith(pair_graph) - ) - if contract_ok and graph_ok: - matched = True - break - if not matched: + if not any( + (ctrct, graph) in graph_keys + for ctrct in (contract_name, contract_name_norm) + for graph in (graph_name, graph_name_norm) + ): continue elif not contract_name or not graph_name: # Fallback mode (no applied vnsGraphInst): skip incomplete contexts. @@ -6825,31 +6819,40 @@ def vnsrscifattn_missing_check(tversion, **kwargs): if tcl == "vnsLIf" or lif_dn_pattern.search(tdn): lif_dns_to_check.add(tdn) + match = lif_dn_pattern.search(tdn) + if match: + lif_device_prefixes.add("uni/tn-{}/lDevVip-{}".format(match.group("tenant"), match.group("device"))) elif tcl == "vnsLDevIfLIf" or ("/lDevIf-[" in tdn and "/lDevIfLIf-" in tdn): converted_match = ldeviflif_pattern.search(tdn) if converted_match: - lif_dns_to_check.add("{}/lIf-{}".format(converted_match.group("base"), converted_match.group("lif"))) + base_dn = converted_match.group("base") + lif_dns_to_check.add("{}/lIf-{}".format(base_dn, converted_match.group("lif"))) + lif_device_prefixes.add(base_dn) # Expand candidates to all cluster interfaces under discovered device clusters. - lif_device_prefixes = set() - for lif_dn in lif_dns_to_check: - match = lif_dn_pattern.search(lif_dn) - if not match: - continue - lif_device_prefixes.add("uni/tn-{}/lDevVip-{}".format(match.group("tenant"), match.group("device"))) - if lif_device_prefixes: - vnsLIfs = icurl("class", "vnsLIf.json?rsp-prop-include=config-only") or [] - prefix_filters = tuple("{}/lIf-".format(prefix) for prefix in lif_device_prefixes) - for vnsLIf in vnsLIfs: + for vnsLIf in get_vnsLIfs(): try: lif_dn = vnsLIf["vnsLIf"]["attributes"]["dn"].strip() except (KeyError, TypeError, AttributeError): continue - if lif_dn and lif_dn.startswith(prefix_filters): + if not lif_dn: + continue + + match = lif_dn_pattern.search(lif_dn) + if not match: + continue + + lif_parent_dn = "uni/tn-{}/lDevVip-{}".format(match.group("tenant"), match.group("device")) + if lif_parent_dn in lif_device_prefixes: lif_dns_to_check.add(lif_dn) - for lif_dn in sorted(lif_dns_to_check): + deployed_lif_dns = set(lif_dns_to_check) + if not deployed_lif_dns: + return Result(result=PASS, msg="No deployed service graph interfaces found.", doc_url=doc_url) + + sg_data = [] + for lif_dn in sorted(deployed_lif_dns): lif_query = ( "{}.json?query-target=subtree" "&target-subtree-class=vnsRsCIfAttN" @@ -6873,55 +6876,52 @@ def vnsrscifattn_missing_check(tversion, **kwargs): lif_dn, ]) - if data: - data.sort(key=lambda row: row[-1]) - if vnsRsCIfAtts or vnsRsCIfAttNs: - return Result(result=FAIL_O, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) - data = [] - - if not vnsRsCIfAtts and not vnsRsCIfAttNs: - vnsLIfs = icurl("class", "vnsLIf.json?rsp-prop-include=config-only") - for vnsLIf in vnsLIfs: - try: - lif_dn = vnsLIf["vnsLIf"]["attributes"]["dn"].strip() - except (KeyError, TypeError, AttributeError): - continue - if not lif_dn: - continue + sg_data = data + data = [] - match = lif_dn_pattern.search(lif_dn) - data.append([ - match.group("tenant") if match else "", - match.group("device") if match else "", - match.group("lif") if match else "", - "N/A", - lif_dn, - ]) + vnsRsCIfAtts = icurl("class", "vnsRsCIfAtt.json?rsp-prop-include=config-only") + vnsRsCIfAttNs = icurl("class", "vnsRsCIfAttN.json?rsp-prop-include=config-only") - data.sort(key=lambda row: row[-1]) - if data: - msg = "vnsLIf has neither vnsRsCIfAtt nor vnsRsCIfAttN. Manual verification required to avoid service graph inconsistency." - manual_action = ( - "Under each impacted L4-L7 device cluster interface, manually verify and reattach concrete interfaces as needed." - ) + # service graph enabled and both old and new mapping objects are absent -> FAIL + if not vnsRsCIfAtts and not vnsRsCIfAttNs: + if sg_data: + sg_data.sort(key=lambda row: row[-1]) + msg = "vnsLIf has neither vnsRsCIfAtt nor vnsRsCIfAttN. Missing concrete interface mapping can cause service graph inconsistency." return Result( - result=MANUAL, + result=FAIL_O, msg=msg, - headers=manual_headers, - data=data, - recommended_action=manual_action, + headers=headers, + data=sg_data, + recommended_action=recommended_action, doc_url=doc_url, ) - msg = "Both vnsRsCIfAtt and vnsRsCIfAttN are missing. Manual verification required for service graph interface attachments." - manual_action = ( - "Validate L4-L7 device cluster interfaces and reattach concrete interfaces where needed." + msg = "Both vnsRsCIfAtt and vnsRsCIfAttN are missing. Reattach concrete interface mappings before upgrade." + return Result( + result=FAIL_O, + msg=msg, + headers=headers, + data=[], + recommended_action=recommended_action, + doc_url=doc_url, ) - return Result(result=MANUAL, msg=msg, recommended_action=manual_action, doc_url=doc_url) + # if deployed service graph interfaces are missing vnsRsCIfAttN + # and old mapping objects are absent, this must still be FAIL. + if sg_data and not vnsRsCIfAtts: + sg_data.sort(key=lambda row: row[-1]) + return Result(result=FAIL_O, headers=headers, data=sg_data, recommended_action=recommended_action, doc_url=doc_url) + + # if deployed service graph interfaces are missing vnsRsCIfAttN -> FAIL + if sg_data: + sg_data.sort(key=lambda row: row[-1]) + return Result(result=FAIL_O, headers=headers, data=sg_data, recommended_action=recommended_action, doc_url=doc_url) + + # only new mapping object exists -> PASS if deployed service graph checks are clean if not vnsRsCIfAtts: return Result(result=PASS, msg="No user-configured vnsRsCIfAtt payload found.", doc_url=doc_url) + # old mapping object exists -> validate old/new consistency (scoped to deployed SG when available) new_dn_keys = set() for new_mo in vnsRsCIfAttNs: try: @@ -6939,6 +6939,11 @@ def vnsrscifattn_missing_check(tversion, **kwargs): if not old_dn: continue + old_lif_match = re.search(r"^(uni/tn-[^/]+/lDevVip-[^/]+/lIf-[^/]+)/", old_dn) + old_lif_dn = old_lif_match.group(1) if old_lif_match else "" + if deployed_lif_dns and old_lif_dn and old_lif_dn not in deployed_lif_dns: + continue + if old_dn.replace("/rscIfAttN-[", "/rscIfAtt-[", 1) in new_dn_keys: continue diff --git a/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py b/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py index 24121285..3db36b9d 100644 --- a/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py +++ b/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py @@ -62,7 +62,7 @@ script.NA, [], ), - # 6.0(3d) is affected and should not return N/A + # 6.0(3d) is affected; if service graph is unconfigured, return PASS ( { vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), @@ -72,10 +72,10 @@ vnsLDevCtx_all_api: [], }, "6.0(3d)", - script.MANUAL, + script.PASS, [], ), - # Both vnsRsCIfAtt and vnsRsCIfAttN are missing (no vnsLIf rows) + # Both vnsRsCIfAtt and vnsRsCIfAttN are missing but service graph is unconfigured -> PASS ( { vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), @@ -85,10 +85,10 @@ vnsLDevCtx_all_api: [], }, "6.1(5e)", - script.MANUAL, + script.PASS, [], ), - # Both vnsRsCIfAtt and vnsRsCIfAttN are missing while vnsLIf exists + # Both vnsRsCIfAtt and vnsRsCIfAttN are missing while vnsLIf exists, but service graph is unconfigured -> PASS ( { vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), @@ -98,23 +98,8 @@ vnsLDevCtx_all_api: [], }, "6.1(5e)", - script.MANUAL, - [ - [ - "CSCwj49418", - "test", - "intf-cons", - "N/A", - "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-cons", - ], - [ - "CSCwj49418", - "test", - "intf-prov", - "N/A", - "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov", - ], - ], + script.PASS, + [], ), # Legacy behavior: when vnsRsCIfAtt is absent but vnsRsCIfAttN exists, return PASS ( @@ -140,7 +125,7 @@ script.PASS, [], ), - # One vnsRsCIfAtt relation (cons) missing in vnsRsCIfAttN + # One vnsRsCIfAtt relation (cons) missing in vnsRsCIfAttN, but service graph unconfigured -> PASS ( { vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_match.json"), @@ -149,18 +134,10 @@ vnsLDevCtx_all_api: [], }, "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]]", - ] - ], + script.PASS, + [], ), - # vnsRsCIfAttN is empty; all vnsRsCIfAtt relations are missing + # vnsRsCIfAttN is empty and old relations exist, but service graph unconfigured -> PASS ( { vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_match.json"), @@ -169,23 +146,8 @@ vnsLDevCtx_all_api: [], }, "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]]", - ], - [ - "CSCwj49418", - "test", - "intf-prov", - "prov", - "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov/rscIfAtt-[uni/tn-CSCwj49418/lDevVip-test/cDev-cdev/cIf-[prov]]", - ], - ], + script.PASS, + [], ), # vnsLIf target from vnsLIfCtx relation has no vnsRsCIfAttN subtree ( @@ -305,7 +267,37 @@ ], ], ), - # If both vnsRsCIfAtt and vnsRsCIfAttN are globally empty, result should be MANUAL + # Imported graph label should still map to applied graph and detect missing common interfaces + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), + vnsRsCIfAttN_api: read_data(dir, "vnsRsCIfAttN_match.json"), + vnsLIf_api: read_data(dir, "vnsLIf_common_device_both.json"), + vnsGraphInst_api: read_data(dir, "vnsGraphInst_applied_single.json"), + vnsLDevCtx_all_api: read_data(dir, "vnsLDevCtx_vnsLDevIfLIf_missing_tcl_imported.json"), + vnsLIf_common_cons_subtree_api: [], + vnsLIf_common_prov_subtree_api: [], + }, + "6.1(5e)", + script.FAIL_O, + [ + [ + "common", + "common-device", + "common-cons", + "cons", + "uni/tn-common/lDevVip-common-device/lIf-common-cons", + ], + [ + "common", + "common-device", + "common-prov", + "prov", + "uni/tn-common/lDevVip-common-device/lIf-common-prov", + ], + ], + ), + # If both vnsRsCIfAtt and vnsRsCIfAttN are globally empty, result should be FAIL ( { vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_empty.json"), @@ -317,20 +309,20 @@ vnsLIf_common_prov_subtree_api: [], }, "6.1(5e)", - script.MANUAL, + script.FAIL_O, [ [ "common", "common-device", "common-cons", - "N/A", + "cons", "uni/tn-common/lDevVip-common-device/lIf-common-cons", ], [ "common", "common-device", "common-prov", - "N/A", + "prov", "uni/tn-common/lDevVip-common-device/lIf-common-prov", ], ], diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json b/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json index f42acc55..9145c08d 100644 --- a/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json +++ b/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json @@ -2,15 +2,15 @@ { "vnsLDevCtx": { "attributes": { - "dn": "uni/tn-user/ldevCtx-c-common-epg-epg-g-test-imported-n-N1", + "dn": "uni/tn-user/ldevCtx-c-common-epg-epg-g-test-n-N1", "ctrctNameOrLbl": "common-epg-epg", - "graphNameOrLbl": "test-imported" + "graphNameOrLbl": "test" }, "children": [ { "vnsLIfCtx": { "attributes": { - "dn": "uni/tn-user/ldevCtx-c-common-epg-epg-g-test-imported-n-N1/lifCtx-cons" + "dn": "uni/tn-user/ldevCtx-c-common-epg-epg-g-test-n-N1/lifCtx-cons" }, "children": [ { diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl_imported.json b/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl_imported.json new file mode 100644 index 00000000..bdb5b3fd --- /dev/null +++ b/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl_imported.json @@ -0,0 +1,45 @@ +[ + { + "vnsLDevCtx": { + "attributes": { + "dn": "uni/tn-user/ldevCtx-c-common-epg-epg-g-test-imported-n-N1", + "ctrctNameOrLbl": "common-epg-epg", + "graphNameOrLbl": "test-imported" + }, + "children": [ + { + "vnsLIfCtx": { + "attributes": { + "dn": "uni/tn-user/ldevCtx-c-common-epg-epg-g-test-imported-n-N1/lifCtx-cons" + }, + "children": [ + { + "vnsRsLIfCtxToLIf": { + "attributes": { + "tDn": "uni/tn-user/lDevIf-[uni/tn-common/lDevVip-common-device]/lDevIfLIf-common-cons" + } + } + } + ] + } + }, + { + "vnsLIfCtx": { + "attributes": { + "dn": "uni/tn-user/ldevCtx-c-common-epg-epg-g-test-imported-n-N1/lifCtx-prov" + }, + "children": [ + { + "vnsRsLIfCtxToLIf": { + "attributes": { + "tDn": "uni/tn-user/lDevIf-[uni/tn-common/lDevVip-common-device]/lDevIfLIf-common-cons" + } + } + } + ] + } + } + ] + } + } +] From e81c3bd3f15007c10899a15a3ce2c65f93743403 Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Wed, 1 Jul 2026 13:56:39 +0000 Subject: [PATCH 10/19] Addressed Dev review comments --- aci-preupgrade-validation-script.py | 125 ++++++++++++++---- docs/docs/validations.md | 14 +- ...nterface_config_for_rscifatt_use_check.py} | 6 +- .../vnsGraphInst_applied_single.json | 0 ...DevCtx_vnsLDevIfLIf_missing_rscifattn.json | 0 .../vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json | 0 ...Ctx_vnsLDevIfLIf_missing_tcl_imported.json | 0 .../vnsLDevCtx_vnsLIf_missing_rscifattn.json | 0 .../vnsLIf_common_device_both.json | 0 .../vnsLIf_only.json | 0 .../vnsLIf_subtree_parent_only.json | 0 .../vnsRsCIfAttN_match.json | 0 .../vnsRsCIfAttN_missing_cons.json | 0 .../vnsRsCIfAtt_empty.json | 0 .../vnsRsCIfAtt_match.json | 0 15 files changed, 111 insertions(+), 34 deletions(-) rename tests/checks/{vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py => verify_cluster_interface_config_for_rscifatt_use_check/test_verify_cluster_interface_config_for_rscifatt_use_check.py} (98%) rename tests/checks/{vnsrscifattn_missing_check => verify_cluster_interface_config_for_rscifatt_use_check}/vnsGraphInst_applied_single.json (100%) rename tests/checks/{vnsrscifattn_missing_check => verify_cluster_interface_config_for_rscifatt_use_check}/vnsLDevCtx_vnsLDevIfLIf_missing_rscifattn.json (100%) rename tests/checks/{vnsrscifattn_missing_check => verify_cluster_interface_config_for_rscifatt_use_check}/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json (100%) rename tests/checks/{vnsrscifattn_missing_check => verify_cluster_interface_config_for_rscifatt_use_check}/vnsLDevCtx_vnsLDevIfLIf_missing_tcl_imported.json (100%) rename tests/checks/{vnsrscifattn_missing_check => verify_cluster_interface_config_for_rscifatt_use_check}/vnsLDevCtx_vnsLIf_missing_rscifattn.json (100%) rename tests/checks/{vnsrscifattn_missing_check => verify_cluster_interface_config_for_rscifatt_use_check}/vnsLIf_common_device_both.json (100%) rename tests/checks/{vnsrscifattn_missing_check => verify_cluster_interface_config_for_rscifatt_use_check}/vnsLIf_only.json (100%) rename tests/checks/{vnsrscifattn_missing_check => verify_cluster_interface_config_for_rscifatt_use_check}/vnsLIf_subtree_parent_only.json (100%) rename tests/checks/{vnsrscifattn_missing_check => verify_cluster_interface_config_for_rscifatt_use_check}/vnsRsCIfAttN_match.json (100%) rename tests/checks/{vnsrscifattn_missing_check => verify_cluster_interface_config_for_rscifatt_use_check}/vnsRsCIfAttN_missing_cons.json (100%) rename tests/checks/{vnsrscifattn_missing_check => verify_cluster_interface_config_for_rscifatt_use_check}/vnsRsCIfAtt_empty.json (100%) rename tests/checks/{vnsrscifattn_missing_check => verify_cluster_interface_config_for_rscifatt_use_check}/vnsRsCIfAtt_match.json (100%) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 95e5c3ed..206ad0c4 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6702,14 +6702,18 @@ def stale_dbgacEpgSummaryTask_check(tversion, **kwargs): return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -@check_wrapper(check_title="Check missing vnsRsCIfAttN") -def vnsrscifattn_missing_check(tversion, **kwargs): +@check_wrapper(check_title="Verifying Cluster Interface Configuration For Deprecated RsCIfAtt Object") +def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion=None, **kwargs): result = PASS - headers = ["Tenant", "Device Name", "Cluster Interface", "Missing Concrete Interface", "vnsRsCIfAtt DN"] + headers_pre_603d = ["Tenant", "Device Name", "Cluster Interface", "Missing Concrete Interface", "vnsRsCIfAtt DN"] + headers_post_603d = ["Tenant", "Device Name", "Cluster Interface"] + headers = headers_pre_603d data = [] - recommended_action = ( - "From 6.0(3) release, Object vnsRsCIfAtt is deprecated. Before upgrade, reattach any missing concrete interface mapping under the same L4-L7 device cluster interface so the corresponding vnsRsCIfAttN relation exists." + recommended_action_pre_603d = ( + "Please reattach concrete interfaces again using the UI (without deleting the existing attachment objects): Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Interface → + → Select the respective interface from the drop-down list → Submit" ) + recommended_action_post_603d = "Please review the concrete interface attachments under the flagged device cluster interfaces and reattach them using the UI: Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Interface → + → Select the respective interface from the drop-down list → Submit" + recommended_action = recommended_action_pre_603d doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#check-missing-vnsrscifattn" lif_dn_pattern = re.compile(r"uni/tn-(?P[^/]+)/lDevVip-(?P[^/]+)/lIf-(?P[^/]+)$") @@ -6724,6 +6728,14 @@ def vnsrscifattn_missing_check(tversion, **kwargs): if tversion.older_than("6.0(3d)"): return Result(result=NA, msg=VER_NOT_AFFECTED, doc_url=doc_url) + cversion_is_ge_603d = False + if cversion: + cversion_is_ge_603d = cversion.same_as("6.0(3d)") or cversion.newer_than("6.0(3d)") + + if cversion_is_ge_603d: + recommended_action = recommended_action_post_603d + headers = headers_post_603d + vnsLIfs = None def get_vnsLIfs(): @@ -6732,10 +6744,7 @@ def get_vnsLIfs(): vnsLIfs = icurl("class", "vnsLIf.json?rsp-prop-include=config-only") or [] return vnsLIfs - graph_query = ( - "vnsGraphInst.json?" - "query-target-filter=eq(vnsGraphInst.configSt,\"applied\")" - ) + graph_query = "vnsGraphInst.json?rsp-prop-include=config-only" graph_insts = icurl("class", graph_query) or [] graph_pairs = set() graph_keys = set() @@ -6765,6 +6774,8 @@ def get_vnsLIfs(): )) lif_dns_to_check = set() + lif_source_tenants = {} + device_prefix_source_tenants = {} lif_device_prefixes = set() ldev_ctx_query = ( "vnsLDevCtx.json?" @@ -6784,6 +6795,11 @@ def get_vnsLIfs(): except (KeyError, TypeError, AttributeError): continue + ldev_ctx_tenant = "" + ldev_ctx_tenant_match = tenant_pattern.search(ldev_ctx_dn) + if ldev_ctx_tenant_match: + ldev_ctx_tenant = ldev_ctx_tenant_match.group(1) + if graph_pairs: contract_name_norm = contract_name.split("-", 1)[-1] if "-" in contract_name else contract_name graph_name_norm = graph_name[:-9] if graph_name.endswith("-imported") else graph_name @@ -6819,15 +6835,25 @@ def get_vnsLIfs(): if tcl == "vnsLIf" or lif_dn_pattern.search(tdn): lif_dns_to_check.add(tdn) + if ldev_ctx_tenant: + lif_source_tenants.setdefault(tdn, set()).add(ldev_ctx_tenant) match = lif_dn_pattern.search(tdn) if match: - lif_device_prefixes.add("uni/tn-{}/lDevVip-{}".format(match.group("tenant"), match.group("device"))) + prefix_dn = "uni/tn-{}/lDevVip-{}".format(match.group("tenant"), match.group("device")) + lif_device_prefixes.add(prefix_dn) + if ldev_ctx_tenant: + device_prefix_source_tenants.setdefault(prefix_dn, set()).add(ldev_ctx_tenant) elif tcl == "vnsLDevIfLIf" or ("/lDevIf-[" in tdn and "/lDevIfLIf-" in tdn): converted_match = ldeviflif_pattern.search(tdn) if converted_match: base_dn = converted_match.group("base") - lif_dns_to_check.add("{}/lIf-{}".format(base_dn, converted_match.group("lif"))) + converted_lif_dn = "{}/lIf-{}".format(base_dn, converted_match.group("lif")) + lif_dns_to_check.add(converted_lif_dn) + if ldev_ctx_tenant: + lif_source_tenants.setdefault(converted_lif_dn, set()).add(ldev_ctx_tenant) lif_device_prefixes.add(base_dn) + if ldev_ctx_tenant: + device_prefix_source_tenants.setdefault(base_dn, set()).add(ldev_ctx_tenant) # Expand candidates to all cluster interfaces under discovered device clusters. if lif_device_prefixes: @@ -6846,12 +6872,16 @@ def get_vnsLIfs(): lif_parent_dn = "uni/tn-{}/lDevVip-{}".format(match.group("tenant"), match.group("device")) if lif_parent_dn in lif_device_prefixes: lif_dns_to_check.add(lif_dn) + source_tenants = device_prefix_source_tenants.get(lif_parent_dn, set()) + if source_tenants: + lif_source_tenants.setdefault(lif_dn, set()).update(source_tenants) deployed_lif_dns = set(lif_dns_to_check) if not deployed_lif_dns: return Result(result=PASS, msg="No deployed service graph interfaces found.", doc_url=doc_url) sg_data = [] + has_implicit_objects = False for lif_dn in sorted(deployed_lif_dns): lif_query = ( "{}.json?query-target=subtree" @@ -6876,16 +6906,33 @@ def get_vnsLIfs(): lif_dn, ]) + if cversion_is_ge_603d and match and match.group("tenant") == "common": + for src_tenant in lif_source_tenants.get(lif_dn, set()): + if src_tenant and src_tenant != "common": + has_implicit_objects = True + break + sg_data = data data = [] + if sg_data: + sg_data.sort(key=lambda row: row[-1]) - vnsRsCIfAtts = icurl("class", "vnsRsCIfAtt.json?rsp-prop-include=config-only") - vnsRsCIfAttNs = icurl("class", "vnsRsCIfAttN.json?rsp-prop-include=config-only") + vnsRsCIfAtts = [] + vnsRsCIfAttNs = icurl("class", "vnsRsCIfAttN.json?rsp-prop-include=config-only") or [] + if not cversion_is_ge_603d: + vnsRsCIfAtts = icurl("class", "vnsRsCIfAtt.json?rsp-prop-include=config-only") - # service graph enabled and both old and new mapping objects are absent -> FAIL - if not vnsRsCIfAtts and not vnsRsCIfAttNs: + if cversion_is_ge_603d: if sg_data: - sg_data.sort(key=lambda row: row[-1]) + sg_data_post_603d = [[row[0], row[1], row[2]] for row in sg_data] + msg = "Graph is rendered with implicit objects" if has_implicit_objects else "vnsRsCIfAttN is missing under deployed L4-L7 cluster interfaces." + return Result(result=FAIL_O, msg=msg, headers=headers, data=sg_data_post_603d, recommended_action=recommended_action, doc_url=doc_url) + return Result(result=PASS, msg="All deployed service graph interfaces have vnsRsCIfAttN.", doc_url=doc_url) + + # If deployed service graph interfaces are missing vnsRsCIfAttN -> FAIL. + # Keep explicit case where old and new are both absent for pre-6.0(3d). + if sg_data: + if not vnsRsCIfAtts and not vnsRsCIfAttNs: msg = "vnsLIf has neither vnsRsCIfAtt nor vnsRsCIfAttN. Missing concrete interface mapping can cause service graph inconsistency." return Result( result=FAIL_O, @@ -6896,6 +6943,10 @@ def get_vnsLIfs(): doc_url=doc_url, ) + return Result(result=FAIL_O, headers=headers, data=sg_data, recommended_action=recommended_action, doc_url=doc_url) + + # Service graph enabled and both old and new mapping objects are absent -> FAIL. + if not vnsRsCIfAtts and not vnsRsCIfAttNs: msg = "Both vnsRsCIfAtt and vnsRsCIfAttN are missing. Reattach concrete interface mappings before upgrade." return Result( result=FAIL_O, @@ -6906,16 +6957,38 @@ def get_vnsLIfs(): doc_url=doc_url, ) - # if deployed service graph interfaces are missing vnsRsCIfAttN - # and old mapping objects are absent, this must still be FAIL. - if sg_data and not vnsRsCIfAtts: - sg_data.sort(key=lambda row: row[-1]) - return Result(result=FAIL_O, headers=headers, data=sg_data, recommended_action=recommended_action, doc_url=doc_url) + # For current version below 6.0(3d), do not auto-PASS when old MO is absent. + # Validate class-level new MO coverage for all deployed service-graph interfaces. + new_lif_dns = set() + for new_mo in vnsRsCIfAttNs: + try: + new_dn = new_mo["vnsRsCIfAttN"]["attributes"]["dn"].strip() + except (KeyError, TypeError, AttributeError): + continue + lif_match = re.search(r"^(uni/tn-[^/]+/lDevVip-[^/]+/lIf-[^/]+)/rscIfAttN-\[", new_dn) + if lif_match: + new_lif_dns.add(lif_match.group(1)) - # if deployed service graph interfaces are missing vnsRsCIfAttN -> FAIL - if sg_data: - sg_data.sort(key=lambda row: row[-1]) - return Result(result=FAIL_O, headers=headers, data=sg_data, recommended_action=recommended_action, doc_url=doc_url) + missing_new_data = [] + for lif_dn in sorted(deployed_lif_dns): + if lif_dn in new_lif_dns: + continue + + match = lif_dn_pattern.search(lif_dn) + missing_cif = "" + if match: + lif_name = match.group("lif") + missing_cif = lif_name.rsplit("-", 1)[-1] if "-" in lif_name else lif_name + missing_new_data.append([ + match.group("tenant") if match else "", + match.group("device") if match else "", + match.group("lif") if match else "", + missing_cif, + lif_dn, + ]) + + if missing_new_data: + return Result(result=FAIL_O, headers=headers, data=missing_new_data, recommended_action=recommended_action, doc_url=doc_url) # only new mapping object exists -> PASS if deployed service graph checks are clean if not vnsRsCIfAtts: @@ -7060,7 +7133,7 @@ class CheckManager: fabric_link_redundancy_check, apic_downgrade_compat_warning_check, svccore_excessive_data_check, - vnsrscifattn_missing_check, + verify_cluster_interface_config_for_rscifatt_use_check, # Faults apic_disk_space_faults_check, diff --git a/docs/docs/validations.md b/docs/docs/validations.md index d5aef3d0..29336120 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -2807,13 +2807,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. -### Check missing vnsRsCIfAttN +### Verifying Cluster Interface Configuration For Deprecated RsCIfAtt Object -When upgrading to 6.0(3) and above, 'vnsRsCIfAtt' is deleted without creating 'vnsRsCIfAttN' under 'vnsLIf'. This leaves the service graph interface attachment in an inconsistent state. +The flagged device cluster interfaces have one or more concrete interface attachments that use the deprecated relation object vnsRsCIfAtt. This relation will be deleted when upgrading to version 6.0(3) or later. To avoid issues, the affected cluster interfaces must be migrated to the newer relation object, vnsRsCIfAttN. -For all impacted DNs in this check, reattach the Concrete interfaces associated with the cluster interface under Devices in the Services > L4-L7 tab. +To perform the migration, reattach the flagged concrete interface attachments (without deleting the existing attachment objects) using the UI by following these steps: -Tenant --> Services --> L4-L7 --> Devices (Device_name) --> cluster interface --> Concrete interfaces +Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Interface → + + +The flagged device cluster interfaces have one or more concrete interface attachments that may be missing a valid relation object (vnsRsCIfAttN). This could be a symptom of removal of the deprecated relation object during an upgrade to version 6.0(3) or later. + +It is highly recommended to review the concrete interface attachments under the flagged device cluster interfaces. To ensure that the required relation object is created, reattach the flagged concrete interface attachments (without deleting the existing attachment objects) using the UI by following these steps: + +Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Interface → + ### WRED with Affected FM Models diff --git a/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/test_verify_cluster_interface_config_for_rscifatt_use_check.py similarity index 98% rename from tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py rename to tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/test_verify_cluster_interface_config_for_rscifatt_use_check.py index 3db36b9d..609f849c 100644 --- a/tests/checks/vnsrscifattn_missing_check/test_vnsrscifattn_missing_check.py +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/test_verify_cluster_interface_config_for_rscifatt_use_check.py @@ -7,15 +7,13 @@ dir = os.path.dirname(os.path.abspath(__file__)) -test_function = "vnsrscifattn_missing_check" +test_function = "verify_cluster_interface_config_for_rscifatt_use_check" # icurl queries vnsRsCIfAtt_api = "vnsRsCIfAtt.json?rsp-prop-include=config-only" vnsRsCIfAttN_api = "vnsRsCIfAttN.json?rsp-prop-include=config-only" vnsLIf_api = "vnsLIf.json?rsp-prop-include=config-only" -vnsGraphInst_api = ( - 'vnsGraphInst.json?query-target-filter=eq(vnsGraphInst.configSt,"applied")' -) +vnsGraphInst_api = 'vnsGraphInst.json?rsp-prop-include=config-only' vnsLDevCtx_all_api = ( 'vnsLDevCtx.json?' 'rsp-prop-include=config-only' diff --git a/tests/checks/vnsrscifattn_missing_check/vnsGraphInst_applied_single.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsGraphInst_applied_single.json similarity index 100% rename from tests/checks/vnsrscifattn_missing_check/vnsGraphInst_applied_single.json rename to tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsGraphInst_applied_single.json diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_rscifattn.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLDevIfLIf_missing_rscifattn.json similarity index 100% rename from tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_rscifattn.json rename to tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLDevIfLIf_missing_rscifattn.json diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json similarity index 100% rename from tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json rename to tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl_imported.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl_imported.json similarity index 100% rename from tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl_imported.json rename to tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl_imported.json diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLIf_missing_rscifattn.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLIf_missing_rscifattn.json similarity index 100% rename from tests/checks/vnsrscifattn_missing_check/vnsLDevCtx_vnsLIf_missing_rscifattn.json rename to tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLIf_missing_rscifattn.json diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLIf_common_device_both.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLIf_common_device_both.json similarity index 100% rename from tests/checks/vnsrscifattn_missing_check/vnsLIf_common_device_both.json rename to tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLIf_common_device_both.json diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLIf_only.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLIf_only.json similarity index 100% rename from tests/checks/vnsrscifattn_missing_check/vnsLIf_only.json rename to tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLIf_only.json diff --git a/tests/checks/vnsrscifattn_missing_check/vnsLIf_subtree_parent_only.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLIf_subtree_parent_only.json similarity index 100% rename from tests/checks/vnsrscifattn_missing_check/vnsLIf_subtree_parent_only.json rename to tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLIf_subtree_parent_only.json diff --git a/tests/checks/vnsrscifattn_missing_check/vnsRsCIfAttN_match.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAttN_match.json similarity index 100% rename from tests/checks/vnsrscifattn_missing_check/vnsRsCIfAttN_match.json rename to tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAttN_match.json diff --git a/tests/checks/vnsrscifattn_missing_check/vnsRsCIfAttN_missing_cons.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAttN_missing_cons.json similarity index 100% rename from tests/checks/vnsrscifattn_missing_check/vnsRsCIfAttN_missing_cons.json rename to tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAttN_missing_cons.json diff --git a/tests/checks/vnsrscifattn_missing_check/vnsRsCIfAtt_empty.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAtt_empty.json similarity index 100% rename from tests/checks/vnsrscifattn_missing_check/vnsRsCIfAtt_empty.json rename to tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAtt_empty.json diff --git a/tests/checks/vnsrscifattn_missing_check/vnsRsCIfAtt_match.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAtt_match.json similarity index 100% rename from tests/checks/vnsrscifattn_missing_check/vnsRsCIfAtt_match.json rename to tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAtt_match.json From 74acfe96228c20f64475e10236055dc7e0de588a Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Thu, 2 Jul 2026 06:52:57 +0000 Subject: [PATCH 11/19] Addressed the review comments --- aci-preupgrade-validation-script.py | 418 +++++++----------- docs/docs/validations.md | 4 + ...interface_config_for_rscifatt_use_check.py | 1 + 3 files changed, 160 insertions(+), 263 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 206ad0c4..4aef784b 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6703,338 +6703,230 @@ def stale_dbgacEpgSummaryTask_check(tversion, **kwargs): @check_wrapper(check_title="Verifying Cluster Interface Configuration For Deprecated RsCIfAtt Object") -def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion=None, **kwargs): +def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, **kwargs): result = PASS - headers_pre_603d = ["Tenant", "Device Name", "Cluster Interface", "Missing Concrete Interface", "vnsRsCIfAtt DN"] - headers_post_603d = ["Tenant", "Device Name", "Cluster Interface"] - headers = headers_pre_603d - data = [] - recommended_action_pre_603d = ( - "Please reattach concrete interfaces again using the UI (without deleting the existing attachment objects): Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Interface → + → Select the respective interface from the drop-down list → Submit" - ) - recommended_action_post_603d = "Please review the concrete interface attachments under the flagged device cluster interfaces and reattach them using the UI: Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Interface → + → Select the respective interface from the drop-down list → Submit" - recommended_action = recommended_action_pre_603d doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#check-missing-vnsrscifattn" - lif_dn_pattern = re.compile(r"uni/tn-(?P[^/]+)/lDevVip-(?P[^/]+)/lIf-(?P[^/]+)$") - ldeviflif_pattern = re.compile( - r"^uni/tn-[^/]+/lDevIf-\[(?Puni/tn-[^\]]+/lDevVip-[^\]]+)\]/lDevIfLIf-(?P[^/]+)$" - ) - tenant_pattern = re.compile(r"^uni/tn-([^/]+)/") + lif_dn_regex = r"uni/tn-(?P[^/]+)/lDevVip-(?P[^/]+)/lIf-(?P[^/]+)$" + ldeviflif_regex = r"^uni/tn-[^/]+/lDevIf-\[(?Puni/tn-[^\]]+/lDevVip-[^\]]+)\]/lDevIfLIf-(?P[^/]+)$" + tn_regex = r"^uni/tn-([^/]+)/" 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) - cversion_is_ge_603d = False - if cversion: - cversion_is_ge_603d = cversion.same_as("6.0(3d)") or cversion.newer_than("6.0(3d)") - - if cversion_is_ge_603d: - recommended_action = recommended_action_post_603d - headers = headers_post_603d - - vnsLIfs = None + post_upgrade = bool(cversion and (cversion.same_as("6.0(3d)") or cversion.newer_than("6.0(3d)"))) + if post_upgrade: + headers = ["Tenant", "Device Name", "Cluster Interface"] + recommended_action = "Please review the concrete interface attachments under the flagged device cluster interfaces and reattach them using the UI: Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Interface → + → Select the respective interface from the drop-down list → Submit" + else: + headers = ["Tenant", "Device Name", "Cluster Interface", "Missing Concrete Interface", "vnsRsCIfAtt DN"] + recommended_action = "Please reattach concrete interfaces again using the UI (without deleting the existing attachment objects): Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Interface → + → Select the respective interface from the drop-down list → Submit" - def get_vnsLIfs(): - nonlocal vnsLIfs - if vnsLIfs is None: - vnsLIfs = icurl("class", "vnsLIf.json?rsp-prop-include=config-only") or [] - return vnsLIfs + # ── Step 1: Collect deployed service-graph LIF DNs ────────────────────── - graph_query = "vnsGraphInst.json?rsp-prop-include=config-only" - graph_insts = icurl("class", graph_query) or [] - graph_pairs = set() + # Build (contract_name, graph_name) keys from applied vnsGraphInst objects graph_keys = set() - for graph_entry in graph_insts: + for entry in icurl("class", "vnsGraphInst.json?rsp-prop-include=config-only") or []: try: - ctrct_dn = graph_entry["vnsGraphInst"]["attributes"]["ctrctDn"].strip() - graph_dn = graph_entry["vnsGraphInst"]["attributes"]["graphDn"].strip() + ctrct_dn = entry["vnsGraphInst"]["attributes"]["ctrctDn"].strip() + graph_dn = entry["vnsGraphInst"]["attributes"]["graphDn"].strip() except (KeyError, TypeError, AttributeError): continue - if not ctrct_dn or not graph_dn: - continue + c = re.search(r"/brc-([^/]+)$", ctrct_dn) + g = re.search(r"/AbsGraph-([^/]+)$", graph_dn) + if c and g: + graph_keys.add((c.group(1), g.group(1))) - contract_match = re.search(r"/brc-([^/]+)$", ctrct_dn) - graph_match = re.search(r"/AbsGraph-([^/]+)$", graph_dn) - tenant_match = tenant_pattern.search(ctrct_dn) - if not contract_match or not graph_match: - continue + lif_dns = set() + lif_source_tenants = {} # lif_dn -> set(contract tenants) for implicit-object detection + dev_source_tenants = {} # device-prefix DN -> set(contract tenants) - graph_pairs.add(( - tenant_match.group(1) if tenant_match else "", - contract_match.group(1), - graph_match.group(1), - )) - graph_keys.add(( - contract_match.group(1), - graph_match.group(1), - )) - - lif_dns_to_check = set() - lif_source_tenants = {} - device_prefix_source_tenants = {} - lif_device_prefixes = set() ldev_ctx_query = ( - "vnsLDevCtx.json?" - "rsp-prop-include=config-only" + "vnsLDevCtx.json?rsp-prop-include=config-only" "&rsp-subtree=full" "&rsp-subtree-class=vnsLIfCtx,vnsRsLIfCtxToLIf" "&rsp-subtree-include=required" ) - ldev_ctxs = icurl("class", ldev_ctx_query) or [] - for ldev_ctx_entry in ldev_ctxs: - try: - ldev_ctx_mo = ldev_ctx_entry["vnsLDevCtx"] - attrs = ldev_ctx_mo["attributes"] - ldev_ctx_dn = attrs["dn"].strip() - contract_name = attrs["ctrctNameOrLbl"].strip() - graph_name = attrs["graphNameOrLbl"].strip() - except (KeyError, TypeError, AttributeError): - continue + for entry in icurl("class", ldev_ctx_query) or []: + try: + mo = entry["vnsLDevCtx"] + attrs = mo["attributes"] + contract = attrs["ctrctNameOrLbl"].strip() + graph = attrs["graphNameOrLbl"].strip() + ctx_dn = attrs["dn"].strip() + except (KeyError, TypeError, AttributeError): + continue - ldev_ctx_tenant = "" - ldev_ctx_tenant_match = tenant_pattern.search(ldev_ctx_dn) - if ldev_ctx_tenant_match: - ldev_ctx_tenant = ldev_ctx_tenant_match.group(1) - - if graph_pairs: - contract_name_norm = contract_name.split("-", 1)[-1] if "-" in contract_name else contract_name - graph_name_norm = graph_name[:-9] if graph_name.endswith("-imported") else graph_name - if not any( - (ctrct, graph) in graph_keys - for ctrct in (contract_name, contract_name_norm) - for graph in (graph_name, graph_name_norm) - ): - continue - elif not contract_name or not graph_name: - # Fallback mode (no applied vnsGraphInst): skip incomplete contexts. + # Filter to contexts matching an active graph (fall back to all if no graphs found) + if graph_keys: + c_norm = contract.split("-", 1)[-1] if "-" in contract else contract + g_norm = graph[:-9] if graph.endswith("-imported") else graph + if not any((c, g) in graph_keys for c in (contract, c_norm) for g in (graph, g_norm)): continue + elif not contract or not graph: + continue # Fallback mode: skip incomplete contexts + + ctx_tenant_m = re.search(tn_regex, ctx_dn) + ctx_tenant = ctx_tenant_m.group(1) if ctx_tenant_m else "" + + # DFS through vnsLIfCtx children to collect vnsRsLIfCtxToLIf references + stack = [mo] + while stack: + cur = stack.pop() + for child in cur.get("children", []) or []: + if child.get("vnsLIfCtx"): + stack.append(child["vnsLIfCtx"]) + rs = child.get("vnsRsLIfCtxToLIf") + if not rs: + continue + try: + tcl = rs["attributes"].get("tCl", "") + tdn = rs["attributes"]["tDn"].strip() + except (KeyError, TypeError, AttributeError): + continue + if not tdn: + continue - stack = [ldev_ctx_mo] - while stack: - current_mo = stack.pop() - for child in current_mo.get("children", []) or []: - lifctx_mo = child.get("vnsLIfCtx") - if lifctx_mo: - stack.append(lifctx_mo) - - rs_lifctx_to_lif = child.get("vnsRsLIfCtxToLIf") - if not rs_lifctx_to_lif: - continue - - try: - tcl = rs_lifctx_to_lif["attributes"].get("tCl", "") - tdn = rs_lifctx_to_lif["attributes"]["tDn"].strip() - except (KeyError, TypeError, AttributeError): - continue - if not tdn: - continue - - if tcl == "vnsLIf" or lif_dn_pattern.search(tdn): - lif_dns_to_check.add(tdn) - if ldev_ctx_tenant: - lif_source_tenants.setdefault(tdn, set()).add(ldev_ctx_tenant) - match = lif_dn_pattern.search(tdn) - if match: - prefix_dn = "uni/tn-{}/lDevVip-{}".format(match.group("tenant"), match.group("device")) - lif_device_prefixes.add(prefix_dn) - if ldev_ctx_tenant: - device_prefix_source_tenants.setdefault(prefix_dn, set()).add(ldev_ctx_tenant) - elif tcl == "vnsLDevIfLIf" or ("/lDevIf-[" in tdn and "/lDevIfLIf-" in tdn): - converted_match = ldeviflif_pattern.search(tdn) - if converted_match: - base_dn = converted_match.group("base") - converted_lif_dn = "{}/lIf-{}".format(base_dn, converted_match.group("lif")) - lif_dns_to_check.add(converted_lif_dn) - if ldev_ctx_tenant: - lif_source_tenants.setdefault(converted_lif_dn, set()).add(ldev_ctx_tenant) - lif_device_prefixes.add(base_dn) - if ldev_ctx_tenant: - device_prefix_source_tenants.setdefault(base_dn, set()).add(ldev_ctx_tenant) - - # Expand candidates to all cluster interfaces under discovered device clusters. - if lif_device_prefixes: - for vnsLIf in get_vnsLIfs(): + if tcl == "vnsLIf" or re.search(lif_dn_regex, tdn): + lif_dns.add(tdn) + lm = re.search(lif_dn_regex, tdn) + if lm and ctx_tenant: + dev_dn = "uni/tn-{}/lDevVip-{}".format(lm.group("tenant"), lm.group("device")) + lif_source_tenants.setdefault(tdn, set()).add(ctx_tenant) + dev_source_tenants.setdefault(dev_dn, set()).add(ctx_tenant) + elif tcl == "vnsLDevIfLIf" or ("/lDevIf-[" in tdn and "/lDevIfLIf-" in tdn): + cm = re.search(ldeviflif_regex, tdn) + if cm: + converted = "{}/lIf-{}".format(cm.group("base"), cm.group("lif")) + lif_dns.add(converted) + if ctx_tenant: + lif_source_tenants.setdefault(converted, set()).add(ctx_tenant) + dev_source_tenants.setdefault(cm.group("base"), set()).add(ctx_tenant) + + # Expand to all LIFs under the same device clusters + dev_prefixes = set() + for lif_dn in lif_dns: + lm = re.search(lif_dn_regex, lif_dn) + if lm: + dev_prefixes.add("uni/tn-{}/lDevVip-{}".format(lm.group("tenant"), lm.group("device"))) + + if dev_prefixes: + for entry in icurl("class", "vnsLIf.json?rsp-prop-include=config-only") or []: try: - lif_dn = vnsLIf["vnsLIf"]["attributes"]["dn"].strip() + lif_dn = entry["vnsLIf"]["attributes"]["dn"].strip() except (KeyError, TypeError, AttributeError): continue - if not lif_dn: - continue - - match = lif_dn_pattern.search(lif_dn) - if not match: + lm = re.search(lif_dn_regex, lif_dn) + if not lm: continue + dev_dn = "uni/tn-{}/lDevVip-{}".format(lm.group("tenant"), lm.group("device")) + if dev_dn in dev_prefixes: + lif_dns.add(lif_dn) + if dev_source_tenants.get(dev_dn): + lif_source_tenants.setdefault(lif_dn, set()).update(dev_source_tenants[dev_dn]) - lif_parent_dn = "uni/tn-{}/lDevVip-{}".format(match.group("tenant"), match.group("device")) - if lif_parent_dn in lif_device_prefixes: - lif_dns_to_check.add(lif_dn) - source_tenants = device_prefix_source_tenants.get(lif_parent_dn, set()) - if source_tenants: - lif_source_tenants.setdefault(lif_dn, set()).update(source_tenants) - - deployed_lif_dns = set(lif_dns_to_check) - if not deployed_lif_dns: + if not lif_dns: return Result(result=PASS, msg="No deployed service graph interfaces found.", doc_url=doc_url) + # ── Step 2: Per-LIF subtree check for missing vnsRsCIfAttN ────────────── + sg_data = [] has_implicit_objects = False - for lif_dn in sorted(deployed_lif_dns): - lif_query = ( - "{}.json?query-target=subtree" - "&target-subtree-class=vnsRsCIfAttN" - "&rsp-prop-include=config-only" - ).format(lif_dn) + for lif_dn in sorted(lif_dns): + lif_query = "{}.json?query-target=subtree&target-subtree-class=vnsRsCIfAttN&rsp-prop-include=config-only".format(lif_dn) lif_subtree = icurl("mo", lif_query) or [] - has_rscifattn = any(isinstance(mo, dict) and mo.get("vnsRsCIfAttN") for mo in lif_subtree) - if has_rscifattn: + if any(isinstance(mo, dict) and mo.get("vnsRsCIfAttN") for mo in lif_subtree): continue + lm = re.search(lif_dn_regex, lif_dn) + lif_name = lm.group("lif") if lm else "" + missing_cif = lif_name.rsplit("-", 1)[-1] if "-" in lif_name else lif_name + sg_data.append([lm.group("tenant") if lm else "", lm.group("device") if lm else "", lif_name, missing_cif, lif_dn]) + if post_upgrade and lm and lm.group("tenant") == "common": + if any(t and t != "common" for t in lif_source_tenants.get(lif_dn, set())): + has_implicit_objects = True - match = lif_dn_pattern.search(lif_dn) - missing_cif = "" - if match: - lif_name = match.group("lif") - missing_cif = lif_name.rsplit("-", 1)[-1] if "-" in lif_name else lif_name - data.append([ - match.group("tenant") if match else "", - match.group("device") if match else "", - match.group("lif") if match else "", - missing_cif, - lif_dn, - ]) - - if cversion_is_ge_603d and match and match.group("tenant") == "common": - for src_tenant in lif_source_tenants.get(lif_dn, set()): - if src_tenant and src_tenant != "common": - has_implicit_objects = True - break + # ── Step 3: Return results ─────────────────────────────────────────────── - sg_data = data - data = [] - if sg_data: - sg_data.sort(key=lambda row: row[-1]) - - vnsRsCIfAtts = [] - vnsRsCIfAttNs = icurl("class", "vnsRsCIfAttN.json?rsp-prop-include=config-only") or [] - if not cversion_is_ge_603d: - vnsRsCIfAtts = icurl("class", "vnsRsCIfAtt.json?rsp-prop-include=config-only") - - if cversion_is_ge_603d: + if post_upgrade: if sg_data: - sg_data_post_603d = [[row[0], row[1], row[2]] for row in sg_data] + sg_data.sort(key=lambda r: r[-1]) msg = "Graph is rendered with implicit objects" if has_implicit_objects else "vnsRsCIfAttN is missing under deployed L4-L7 cluster interfaces." - return Result(result=FAIL_O, msg=msg, headers=headers, data=sg_data_post_603d, recommended_action=recommended_action, doc_url=doc_url) + return Result(result=FAIL_O, msg=msg, headers=headers, data=[[r[0], r[1], r[2]] for r in sg_data], recommended_action=recommended_action, doc_url=doc_url) return Result(result=PASS, msg="All deployed service graph interfaces have vnsRsCIfAttN.", doc_url=doc_url) - # If deployed service graph interfaces are missing vnsRsCIfAttN -> FAIL. - # Keep explicit case where old and new are both absent for pre-6.0(3d). - if sg_data: - if not vnsRsCIfAtts and not vnsRsCIfAttNs: - msg = "vnsLIf has neither vnsRsCIfAtt nor vnsRsCIfAttN. Missing concrete interface mapping can cause service graph inconsistency." - return Result( - result=FAIL_O, - msg=msg, - headers=headers, - data=sg_data, - recommended_action=recommended_action, - doc_url=doc_url, - ) + # Pre-upgrade: fetch both relation object classes for further checks + vnsRsCIfAttNs = icurl("class", "vnsRsCIfAttN.json?rsp-prop-include=config-only") or [] + vnsRsCIfAtts = icurl("class", "vnsRsCIfAtt.json?rsp-prop-include=config-only") or [] - return Result(result=FAIL_O, headers=headers, data=sg_data, recommended_action=recommended_action, doc_url=doc_url) + if sg_data: + sg_data.sort(key=lambda r: r[-1]) + msg = "vnsLIf has neither vnsRsCIfAtt nor vnsRsCIfAttN. Missing concrete interface mapping can cause service graph inconsistency." if not vnsRsCIfAtts and not vnsRsCIfAttNs else "" + return Result(result=FAIL_O, msg=msg, headers=headers, data=sg_data, recommended_action=recommended_action, doc_url=doc_url) - # Service graph enabled and both old and new mapping objects are absent -> FAIL. if not vnsRsCIfAtts and not vnsRsCIfAttNs: - msg = "Both vnsRsCIfAtt and vnsRsCIfAttN are missing. Reattach concrete interface mappings before upgrade." - return Result( - result=FAIL_O, - msg=msg, - headers=headers, - data=[], - recommended_action=recommended_action, - doc_url=doc_url, - ) + return Result(result=FAIL_O, msg="Both vnsRsCIfAtt and vnsRsCIfAttN are missing. Reattach concrete interface mappings before upgrade.", headers=headers, data=[], recommended_action=recommended_action, doc_url=doc_url) - # For current version below 6.0(3d), do not auto-PASS when old MO is absent. - # Validate class-level new MO coverage for all deployed service-graph interfaces. - new_lif_dns = set() - for new_mo in vnsRsCIfAttNs: - try: - new_dn = new_mo["vnsRsCIfAttN"]["attributes"]["dn"].strip() - except (KeyError, TypeError, AttributeError): - continue - lif_match = re.search(r"^(uni/tn-[^/]+/lDevVip-[^/]+/lIf-[^/]+)/rscIfAttN-\[", new_dn) - if lif_match: - new_lif_dns.add(lif_match.group(1)) - - missing_new_data = [] - for lif_dn in sorted(deployed_lif_dns): - if lif_dn in new_lif_dns: - continue - - match = lif_dn_pattern.search(lif_dn) - missing_cif = "" - if match: - lif_name = match.group("lif") - missing_cif = lif_name.rsplit("-", 1)[-1] if "-" in lif_name else lif_name - missing_new_data.append([ - match.group("tenant") if match else "", - match.group("device") if match else "", - match.group("lif") if match else "", - missing_cif, - lif_dn, - ]) - - if missing_new_data: - return Result(result=FAIL_O, headers=headers, data=missing_new_data, recommended_action=recommended_action, doc_url=doc_url) - - # only new mapping object exists -> PASS if deployed service graph checks are clean if not vnsRsCIfAtts: return Result(result=PASS, msg="No user-configured vnsRsCIfAtt payload found.", doc_url=doc_url) - # old mapping object exists -> validate old/new consistency (scoped to deployed SG when available) - new_dn_keys = set() - for new_mo in vnsRsCIfAttNs: + # Build new-object lookup sets in a single pass over vnsRsCIfAttNs + new_lif_dns = set() # LIF DNs covered by vnsRsCIfAttN (for coverage check) + new_dn_keys = set() # New DNs rewritten as old-style keys (for consistency check) + for mo in vnsRsCIfAttNs: try: - dn = new_mo["vnsRsCIfAttN"]["attributes"]["dn"].strip() + dn = mo["vnsRsCIfAttN"]["attributes"]["dn"].strip() except (KeyError, TypeError, AttributeError): continue - if dn: - new_dn_keys.add(dn.replace("/rscIfAttN-[", "/rscIfAtt-[", 1)) + if not dn: + continue + lm = re.search(r"^(uni/tn-[^/]+/lDevVip-[^/]+/lIf-[^/]+)/rscIfAttN-\[", dn) + if lm: + new_lif_dns.add(lm.group(1)) + new_dn_keys.add(dn.replace("/rscIfAttN-[", "/rscIfAtt-[", 1)) + + # Secondary coverage check: any deployed LIF not yet covered by a vnsRsCIfAttN + data = [] + for lif_dn in sorted(lif_dns): + if lif_dn in new_lif_dns: + continue + lm = re.search(lif_dn_regex, lif_dn) + lif_name = lm.group("lif") if lm else "" + missing_cif = lif_name.rsplit("-", 1)[-1] if "-" in lif_name else lif_name + data.append([lm.group("tenant") if lm else "", lm.group("device") if lm else "", lif_name, missing_cif, lif_dn]) + if data: + return Result(result=FAIL_O, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) - for old_mo in vnsRsCIfAtts: + # Consistency check: each old vnsRsCIfAtt must have a matching new vnsRsCIfAttN + data = [] + for mo in vnsRsCIfAtts: try: - old_dn = old_mo["vnsRsCIfAtt"]["attributes"]["dn"].strip() + old_dn = mo["vnsRsCIfAtt"]["attributes"]["dn"].strip() except (KeyError, TypeError, AttributeError): continue if not old_dn: continue - - old_lif_match = re.search(r"^(uni/tn-[^/]+/lDevVip-[^/]+/lIf-[^/]+)/", old_dn) - old_lif_dn = old_lif_match.group(1) if old_lif_match else "" - if deployed_lif_dns and old_lif_dn and old_lif_dn not in deployed_lif_dns: + lm = re.search(r"^(uni/tn-[^/]+/lDevVip-[^/]+/lIf-[^/]+)/", old_dn) + old_lif = lm.group(1) if lm else "" + if lif_dns and old_lif and old_lif not in lif_dns: continue - if old_dn.replace("/rscIfAttN-[", "/rscIfAtt-[", 1) in new_dn_keys: continue - - match = re.search( + m = re.search( r"uni/tn-(?P[^/]+)/lDevVip-(?P[^/]+)/lIf-(?P[^/]+)/" r"rscIfAtt-\[.*?/cIf-\[(?P[^\]]+)\]\]", old_dn, ) data.append([ - match.group("tenant") if match else "", - match.group("device") if match else "", - match.group("lif") if match else "", - match.group("cif") if match else "", + m.group("tenant") if m else "", + m.group("device") if m else "", + m.group("lif") if m else "", + m.group("cif") if m else "", old_dn, ]) - data.sort(key=lambda row: row[-1]) - + data.sort(key=lambda r: r[-1]) if data: result = FAIL_O diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 29336120..f004e03d 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -2809,12 +2809,16 @@ This check will verify the count of the `svccoreCtrlr` Managed Object and raise ### Verifying Cluster Interface Configuration For Deprecated RsCIfAtt Object +For upgrades from versions 6.0(2) or below to 6.0(3) or above: + The flagged device cluster interfaces have one or more concrete interface attachments that use the deprecated relation object vnsRsCIfAtt. This relation will be deleted when upgrading to version 6.0(3) or later. To avoid issues, the affected cluster interfaces must be migrated to the newer relation object, vnsRsCIfAttN. To perform the migration, reattach the flagged concrete interface attachments (without deleting the existing attachment objects) using the UI by following these steps: Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Interface → + +For upgrades with current version as 6.0(3) or above: + The flagged device cluster interfaces have one or more concrete interface attachments that may be missing a valid relation object (vnsRsCIfAttN). This could be a symptom of removal of the deprecated relation object during an upgrade to version 6.0(3) or later. It is highly recommended to review the concrete interface attachments under the flagged device cluster interfaces. To ensure that the required relation object is created, reattach the flagged concrete interface attachments (without deleting the existing attachment objects) using the UI by following these steps: diff --git a/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/test_verify_cluster_interface_config_for_rscifatt_use_check.py b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/test_verify_cluster_interface_config_for_rscifatt_use_check.py index 609f849c..a39f0a1c 100644 --- a/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/test_verify_cluster_interface_config_for_rscifatt_use_check.py +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/test_verify_cluster_interface_config_for_rscifatt_use_check.py @@ -329,6 +329,7 @@ ) def test_logic(run_check, mock_icurl, tversion, expected_result, expected_data): result = run_check( + cversion=None, tversion=script.AciVersion(tversion) if tversion else None, ) assert result.result == expected_result From c21711544766b0ae6e997ae8a0a11c41c49c263a Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Fri, 3 Jul 2026 05:26:25 +0000 Subject: [PATCH 12/19] Addressed dev comments on contract and interface names with multiple hyphens --- aci-preupgrade-validation-script.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 4aef784b..1fbeb11b 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6759,18 +6759,19 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * except (KeyError, TypeError, AttributeError): continue + ctx_tenant_m = re.search(tn_regex, ctx_dn) + ctx_tenant = ctx_tenant_m.group(1) if ctx_tenant_m else "" + # Filter to contexts matching an active graph (fall back to all if no graphs found) if graph_keys: - c_norm = contract.split("-", 1)[-1] if "-" in contract else contract + contract_parts = [part for part in contract.split("-") if part] + c_norm = "-".join(contract_parts[1:]) if len(contract_parts) > 1 else contract g_norm = graph[:-9] if graph.endswith("-imported") else graph if not any((c, g) in graph_keys for c in (contract, c_norm) for g in (graph, g_norm)): continue elif not contract or not graph: continue # Fallback mode: skip incomplete contexts - ctx_tenant_m = re.search(tn_regex, ctx_dn) - ctx_tenant = ctx_tenant_m.group(1) if ctx_tenant_m else "" - # DFS through vnsLIfCtx children to collect vnsRsLIfCtxToLIf references stack = [mo] while stack: @@ -6841,7 +6842,8 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * continue lm = re.search(lif_dn_regex, lif_dn) lif_name = lm.group("lif") if lm else "" - missing_cif = lif_name.rsplit("-", 1)[-1] if "-" in lif_name else lif_name + lif_parts = [part for part in lif_name.split("-") if part] + missing_cif = lif_parts[-1] if lif_parts else lif_name sg_data.append([lm.group("tenant") if lm else "", lm.group("device") if lm else "", lif_name, missing_cif, lif_dn]) if post_upgrade and lm and lm.group("tenant") == "common": if any(t and t != "common" for t in lif_source_tenants.get(lif_dn, set())): @@ -6893,7 +6895,8 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * continue lm = re.search(lif_dn_regex, lif_dn) lif_name = lm.group("lif") if lm else "" - missing_cif = lif_name.rsplit("-", 1)[-1] if "-" in lif_name else lif_name + lif_parts = [part for part in lif_name.split("-") if part] + missing_cif = lif_parts[-1] if lif_parts else lif_name data.append([lm.group("tenant") if lm else "", lm.group("device") if lm else "", lif_name, missing_cif, lif_dn]) if data: return Result(result=FAIL_O, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) From 0da4087ad3c958d6da44956749105befc214baaa Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Fri, 3 Jul 2026 06:03:42 +0000 Subject: [PATCH 13/19] Rebased the code with latest changes by resolving merge conflicts --- aci-preupgrade-validation-script.py | 2 +- docs/docs/validations.md | 40 +++++++++++++++-------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 1fbeb11b..d41d8e83 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -7112,7 +7112,7 @@ class CheckManager: wred_affected_model_check, n9k_c93180yc_fx3_switch_memory_check, stale_dbgacEpgSummaryTask_check, - vnsrscifattn_missing_check, + verify_cluster_interface_config_for_rscifatt_use_check, ] ssh_checks = [ # General diff --git a/docs/docs/validations.md b/docs/docs/validations.md index f004e03d..b76a0d74 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -62,7 +62,7 @@ Items | This Script [g19]: #apic-downgrade-compatibility-when-crossing-62-release [g20]: #supported-hardware-compatibility [g21]: #svccore-excessive-data-check -[g22]: #check-missing-vnsrscifattn +[g22]: #verifying-cluster-interface-configuration-for-deprecated-rscifatt-object ### Fault Checks Items | Faults | This Script | APIC built-in @@ -208,7 +208,7 @@ Items | Defect | This Script [WRED with Affected FM Models][d35] | CSCwt50713 | :white_check_mark: | :no_entry_sign: [N9K-C93180YC-FX3 Switch Memory Less Than 32GB][d36] | CSCwm42741 | :white_check_mark: | :no_entry_sign: [Stale dbgacEpgSummaryTask Objects][d37] | CSCwt69100 | :white_check_mark: | :no_entry_sign: -[Cleanup vnsRsCIfAtt usage in services][d38] | CSCwr51759 | :white_check_mark: | :no_entry_sign: + [d1]: #ep-announce-compatibility [d2]: #eventmgr-db-size-defect-susceptibility [d3]: #contract-port-22-defect @@ -2807,23 +2807,6 @@ 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. -### Verifying Cluster Interface Configuration For Deprecated RsCIfAtt Object - -For upgrades from versions 6.0(2) or below to 6.0(3) or above: - -The flagged device cluster interfaces have one or more concrete interface attachments that use the deprecated relation object vnsRsCIfAtt. This relation will be deleted when upgrading to version 6.0(3) or later. To avoid issues, the affected cluster interfaces must be migrated to the newer relation object, vnsRsCIfAttN. - -To perform the migration, reattach the flagged concrete interface attachments (without deleting the existing attachment objects) using the UI by following these steps: - -Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Interface → + - -For upgrades with current version as 6.0(3) or above: - -The flagged device cluster interfaces have one or more concrete interface attachments that may be missing a valid relation object (vnsRsCIfAttN). This could be a symptom of removal of the deprecated relation object during an upgrade to version 6.0(3) or later. - -It is highly recommended to review the concrete interface attachments under the flagged device cluster interfaces. To ensure that the required relation object is created, reattach the flagged concrete interface attachments (without deleting the existing attachment objects) using the UI by following these steps: - -Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Interface → + ### WRED with Affected FM Models @@ -2859,6 +2842,25 @@ Affected versions: 6.1(5e) and below, or 6.2(1g). Contact Cisco TAC for next steps. For more details, refer to the workaround in [CSCwt69100][75]. +### Verifying Cluster Interface Configuration For Deprecated RsCIfAtt Object + +For upgrades from versions 6.0(2) or below to 6.0(3) or above: + +The flagged device cluster interfaces have one or more concrete interface attachments that use the deprecated relation object vnsRsCIfAtt. This relation will be deleted when upgrading to version 6.0(3) or later. To avoid issues, the affected cluster interfaces must be migrated to the newer relation object, vnsRsCIfAttN. + +To perform the migration, reattach the flagged concrete interface attachments (without deleting the existing attachment objects) using the UI by following these steps: + +Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Interface → + + +For upgrades with current version as 6.0(3) or above: + +The flagged device cluster interfaces have one or more concrete interface attachments that may be missing a valid relation object (vnsRsCIfAttN). This could be a symptom of removal of the deprecated relation object during an upgrade to version 6.0(3) or later. + +It is highly recommended to review the concrete interface attachments under the flagged device cluster interfaces. To ensure that the required relation object is created, reattach the flagged concrete interface attachments (without deleting the existing attachment objects) using the UI by following these steps: + +Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Interface → + + + [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 From df5f5cb58155e17d9c9bbcbc71f8351f88226a6d Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Fri, 3 Jul 2026 06:25:39 +0000 Subject: [PATCH 14/19] Modified variable names with meaningful variable names --- aci-preupgrade-validation-script.py | 138 ++++++++++++++++------------ 1 file changed, 77 insertions(+), 61 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index d41d8e83..e6a04c15 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6734,10 +6734,10 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * graph_dn = entry["vnsGraphInst"]["attributes"]["graphDn"].strip() except (KeyError, TypeError, AttributeError): continue - c = re.search(r"/brc-([^/]+)$", ctrct_dn) - g = re.search(r"/AbsGraph-([^/]+)$", graph_dn) - if c and g: - graph_keys.add((c.group(1), g.group(1))) + contract_match = re.search(r"/brc-([^/]+)$", ctrct_dn) + graph_match = re.search(r"/AbsGraph-([^/]+)$", graph_dn) + if contract_match and graph_match: + graph_keys.add((contract_match.group(1), graph_match.group(1))) lif_dns = set() lif_source_tenants = {} # lif_dn -> set(contract tenants) for implicit-object detection @@ -6751,67 +6751,71 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * ) for entry in icurl("class", ldev_ctx_query) or []: try: - mo = entry["vnsLDevCtx"] - attrs = mo["attributes"] - contract = attrs["ctrctNameOrLbl"].strip() - graph = attrs["graphNameOrLbl"].strip() - ctx_dn = attrs["dn"].strip() + ldev_ctx_mo = entry["vnsLDevCtx"] + ldev_ctx_attrs = ldev_ctx_mo["attributes"] + contract = ldev_ctx_attrs["ctrctNameOrLbl"].strip() + graph = ldev_ctx_attrs["graphNameOrLbl"].strip() + ctx_dn = ldev_ctx_attrs["dn"].strip() except (KeyError, TypeError, AttributeError): continue - ctx_tenant_m = re.search(tn_regex, ctx_dn) - ctx_tenant = ctx_tenant_m.group(1) if ctx_tenant_m else "" + ctx_tenant_match = re.search(tn_regex, ctx_dn) + ctx_tenant = ctx_tenant_match.group(1) if ctx_tenant_match else "" # Filter to contexts matching an active graph (fall back to all if no graphs found) if graph_keys: contract_parts = [part for part in contract.split("-") if part] - c_norm = "-".join(contract_parts[1:]) if len(contract_parts) > 1 else contract - g_norm = graph[:-9] if graph.endswith("-imported") else graph - if not any((c, g) in graph_keys for c in (contract, c_norm) for g in (graph, g_norm)): + contract_without_prefix = "-".join(contract_parts[1:]) if len(contract_parts) > 1 else contract + graph_base_name = graph[:-9] if graph.endswith("-imported") else graph + if not any( + (contract_name, graph_name) in graph_keys + for contract_name in (contract, contract_without_prefix) + for graph_name in (graph, graph_base_name) + ): continue elif not contract or not graph: continue # Fallback mode: skip incomplete contexts # DFS through vnsLIfCtx children to collect vnsRsLIfCtxToLIf references - stack = [mo] + stack = [ldev_ctx_mo] while stack: - cur = stack.pop() - for child in cur.get("children", []) or []: + current_ctx = stack.pop() + for child in current_ctx.get("children", []) or []: if child.get("vnsLIfCtx"): stack.append(child["vnsLIfCtx"]) - rs = child.get("vnsRsLIfCtxToLIf") - if not rs: + lif_relation = child.get("vnsRsLIfCtxToLIf") + if not lif_relation: continue try: - tcl = rs["attributes"].get("tCl", "") - tdn = rs["attributes"]["tDn"].strip() + target_class = lif_relation["attributes"].get("tCl", "") + target_dn = lif_relation["attributes"]["tDn"].strip() except (KeyError, TypeError, AttributeError): continue - if not tdn: + if not target_dn: continue - if tcl == "vnsLIf" or re.search(lif_dn_regex, tdn): - lif_dns.add(tdn) - lm = re.search(lif_dn_regex, tdn) - if lm and ctx_tenant: - dev_dn = "uni/tn-{}/lDevVip-{}".format(lm.group("tenant"), lm.group("device")) - lif_source_tenants.setdefault(tdn, set()).add(ctx_tenant) + if target_class == "vnsLIf" or re.search(lif_dn_regex, target_dn): + lif_dns.add(target_dn) + lif_dn_match = re.search(lif_dn_regex, target_dn) + if lif_dn_match and ctx_tenant: + dev_dn = "uni/tn-{}/lDevVip-{}".format(lif_dn_match.group("tenant"), lif_dn_match.group("device")) + lif_source_tenants.setdefault(target_dn, set()).add(ctx_tenant) dev_source_tenants.setdefault(dev_dn, set()).add(ctx_tenant) - elif tcl == "vnsLDevIfLIf" or ("/lDevIf-[" in tdn and "/lDevIfLIf-" in tdn): - cm = re.search(ldeviflif_regex, tdn) - if cm: - converted = "{}/lIf-{}".format(cm.group("base"), cm.group("lif")) + elif target_class == "vnsLDevIfLIf" or ("/lDevIf-[" in target_dn and "/lDevIfLIf-" in target_dn): + ldeviflif_match = re.search(ldeviflif_regex, target_dn) + if ldeviflif_match: + converted = "{}/lIf-{}".format(ldeviflif_match.group("base"), ldeviflif_match.group("lif")) lif_dns.add(converted) if ctx_tenant: lif_source_tenants.setdefault(converted, set()).add(ctx_tenant) - dev_source_tenants.setdefault(cm.group("base"), set()).add(ctx_tenant) + dev_source_tenants.setdefault(ldeviflif_match.group("base"), set()).add(ctx_tenant) # Expand to all LIFs under the same device clusters dev_prefixes = set() for lif_dn in lif_dns: - lm = re.search(lif_dn_regex, lif_dn) - if lm: - dev_prefixes.add("uni/tn-{}/lDevVip-{}".format(lm.group("tenant"), lm.group("device"))) + lif_dn_match = re.search(lif_dn_regex, lif_dn) + if lif_dn_match: + dev_prefixes.add("uni/tn-{}/lDevVip-{}".format(lif_dn_match.group("tenant"), lif_dn_match.group("device"))) if dev_prefixes: for entry in icurl("class", "vnsLIf.json?rsp-prop-include=config-only") or []: @@ -6819,10 +6823,10 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * lif_dn = entry["vnsLIf"]["attributes"]["dn"].strip() except (KeyError, TypeError, AttributeError): continue - lm = re.search(lif_dn_regex, lif_dn) - if not lm: + lif_dn_match = re.search(lif_dn_regex, lif_dn) + if not lif_dn_match: continue - dev_dn = "uni/tn-{}/lDevVip-{}".format(lm.group("tenant"), lm.group("device")) + dev_dn = "uni/tn-{}/lDevVip-{}".format(lif_dn_match.group("tenant"), lif_dn_match.group("device")) if dev_dn in dev_prefixes: lif_dns.add(lif_dn) if dev_source_tenants.get(dev_dn): @@ -6838,14 +6842,20 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * for lif_dn in sorted(lif_dns): lif_query = "{}.json?query-target=subtree&target-subtree-class=vnsRsCIfAttN&rsp-prop-include=config-only".format(lif_dn) lif_subtree = icurl("mo", lif_query) or [] - if any(isinstance(mo, dict) and mo.get("vnsRsCIfAttN") for mo in lif_subtree): + if any(isinstance(subtree_mo, dict) and subtree_mo.get("vnsRsCIfAttN") for subtree_mo in lif_subtree): continue - lm = re.search(lif_dn_regex, lif_dn) - lif_name = lm.group("lif") if lm else "" + lif_dn_match = re.search(lif_dn_regex, lif_dn) + lif_name = lif_dn_match.group("lif") if lif_dn_match else "" lif_parts = [part for part in lif_name.split("-") if part] missing_cif = lif_parts[-1] if lif_parts else lif_name - sg_data.append([lm.group("tenant") if lm else "", lm.group("device") if lm else "", lif_name, missing_cif, lif_dn]) - if post_upgrade and lm and lm.group("tenant") == "common": + sg_data.append([ + lif_dn_match.group("tenant") if lif_dn_match else "", + lif_dn_match.group("device") if lif_dn_match else "", + lif_name, + missing_cif, + lif_dn, + ]) + if post_upgrade and lif_dn_match and lif_dn_match.group("tenant") == "common": if any(t and t != "common" for t in lif_source_tenants.get(lif_dn, set())): has_implicit_objects = True @@ -6876,16 +6886,16 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * # Build new-object lookup sets in a single pass over vnsRsCIfAttNs new_lif_dns = set() # LIF DNs covered by vnsRsCIfAttN (for coverage check) new_dn_keys = set() # New DNs rewritten as old-style keys (for consistency check) - for mo in vnsRsCIfAttNs: + for relation_mo in vnsRsCIfAttNs: try: - dn = mo["vnsRsCIfAttN"]["attributes"]["dn"].strip() + dn = relation_mo["vnsRsCIfAttN"]["attributes"]["dn"].strip() except (KeyError, TypeError, AttributeError): continue if not dn: continue - lm = re.search(r"^(uni/tn-[^/]+/lDevVip-[^/]+/lIf-[^/]+)/rscIfAttN-\[", dn) - if lm: - new_lif_dns.add(lm.group(1)) + lif_parent_match = re.search(r"^(uni/tn-[^/]+/lDevVip-[^/]+/lIf-[^/]+)/rscIfAttN-\[", dn) + if lif_parent_match: + new_lif_dns.add(lif_parent_match.group(1)) new_dn_keys.add(dn.replace("/rscIfAttN-[", "/rscIfAtt-[", 1)) # Secondary coverage check: any deployed LIF not yet covered by a vnsRsCIfAttN @@ -6893,39 +6903,45 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * for lif_dn in sorted(lif_dns): if lif_dn in new_lif_dns: continue - lm = re.search(lif_dn_regex, lif_dn) - lif_name = lm.group("lif") if lm else "" + lif_dn_match = re.search(lif_dn_regex, lif_dn) + lif_name = lif_dn_match.group("lif") if lif_dn_match else "" lif_parts = [part for part in lif_name.split("-") if part] missing_cif = lif_parts[-1] if lif_parts else lif_name - data.append([lm.group("tenant") if lm else "", lm.group("device") if lm else "", lif_name, missing_cif, lif_dn]) + data.append([ + lif_dn_match.group("tenant") if lif_dn_match else "", + lif_dn_match.group("device") if lif_dn_match else "", + lif_name, + missing_cif, + lif_dn, + ]) if data: return Result(result=FAIL_O, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) # Consistency check: each old vnsRsCIfAtt must have a matching new vnsRsCIfAttN data = [] - for mo in vnsRsCIfAtts: + for old_relation_mo in vnsRsCIfAtts: try: - old_dn = mo["vnsRsCIfAtt"]["attributes"]["dn"].strip() + old_dn = old_relation_mo["vnsRsCIfAtt"]["attributes"]["dn"].strip() except (KeyError, TypeError, AttributeError): continue if not old_dn: continue - lm = re.search(r"^(uni/tn-[^/]+/lDevVip-[^/]+/lIf-[^/]+)/", old_dn) - old_lif = lm.group(1) if lm else "" + old_lif_match = re.search(r"^(uni/tn-[^/]+/lDevVip-[^/]+/lIf-[^/]+)/", old_dn) + old_lif = old_lif_match.group(1) if old_lif_match else "" if lif_dns and old_lif and old_lif not in lif_dns: continue if old_dn.replace("/rscIfAttN-[", "/rscIfAtt-[", 1) in new_dn_keys: continue - m = re.search( + old_relation_match = re.search( r"uni/tn-(?P[^/]+)/lDevVip-(?P[^/]+)/lIf-(?P[^/]+)/" r"rscIfAtt-\[.*?/cIf-\[(?P[^\]]+)\]\]", old_dn, ) data.append([ - m.group("tenant") if m else "", - m.group("device") if m else "", - m.group("lif") if m else "", - m.group("cif") if m else "", + old_relation_match.group("tenant") if old_relation_match else "", + old_relation_match.group("device") if old_relation_match else "", + old_relation_match.group("lif") if old_relation_match else "", + old_relation_match.group("cif") if old_relation_match else "", old_dn, ]) From de1e487d89fc3ee363dcb7fb126fd602385fb032 Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Fri, 3 Jul 2026 06:34:34 +0000 Subject: [PATCH 15/19] Updated general check name --- docs/docs/validations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/validations.md b/docs/docs/validations.md index b76a0d74..508f5bbf 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -39,7 +39,7 @@ Items | This Script [APIC downgrade compatibility when crossing 6.2 release][g19]| :white_check_mark: | :no_entry_sign: [Supported Hardware Compatibility][g20] | :white_check_mark: | :no_entry_sign: [Svccore Excessive Data Check][g21] | :white_check_mark: | :no_entry_sign: -[Check missing vnsRsCIfAttN][g22] | :white_check_mark: | :no_entry_sign: +[Verifying Cluster Interface Configuration For Deprecated RsCIfAtt Object][g22] | :white_check_mark: | :no_entry_sign: [g1]: #compatibility-target-aci-version [g2]: #compatibility-cimc-version From 24d00e8af37868a4bf4148c3045993b05a29b66d Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Fri, 3 Jul 2026 07:02:14 +0000 Subject: [PATCH 16/19] Removed function name under bugs section and updated validations.md file with right title name --- aci-preupgrade-validation-script.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index e6a04c15..7090b21b 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -7128,7 +7128,6 @@ class CheckManager: wred_affected_model_check, n9k_c93180yc_fx3_switch_memory_check, stale_dbgacEpgSummaryTask_check, - verify_cluster_interface_config_for_rscifatt_use_check, ] ssh_checks = [ # General From 697dee00a6bc545bb808749d8ff8b569cdfc3b5b Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Fri, 3 Jul 2026 18:50:20 +0000 Subject: [PATCH 17/19] Addressed Dev comments and modified variable names with meaningful names --- aci-preupgrade-validation-script.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 7090b21b..e37f522d 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -22,7 +22,7 @@ from textwrap import TextWrapper from getpass import getpass from collections import defaultdict, OrderedDict -from datetime import datetime +from datetime import datetime,timedelta from argparse import ArgumentParser from itertools import chain import threading @@ -6730,11 +6730,11 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * graph_keys = set() for entry in icurl("class", "vnsGraphInst.json?rsp-prop-include=config-only") or []: try: - ctrct_dn = entry["vnsGraphInst"]["attributes"]["ctrctDn"].strip() + contract_dn = entry["vnsGraphInst"]["attributes"]["ctrctDn"].strip() graph_dn = entry["vnsGraphInst"]["attributes"]["graphDn"].strip() except (KeyError, TypeError, AttributeError): continue - contract_match = re.search(r"/brc-([^/]+)$", ctrct_dn) + contract_match = re.search(r"/brc-([^/]+)$", contract_dn) graph_match = re.search(r"/AbsGraph-([^/]+)$", graph_dn) if contract_match and graph_match: graph_keys.add((contract_match.group(1), graph_match.group(1))) @@ -6847,7 +6847,7 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * lif_dn_match = re.search(lif_dn_regex, lif_dn) lif_name = lif_dn_match.group("lif") if lif_dn_match else "" lif_parts = [part for part in lif_name.split("-") if part] - missing_cif = lif_parts[-1] if lif_parts else lif_name + missing_cif = lif_parts[1] if len(lif_parts) > 1 else lif_name sg_data.append([ lif_dn_match.group("tenant") if lif_dn_match else "", lif_dn_match.group("device") if lif_dn_match else "", @@ -6869,8 +6869,8 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * return Result(result=PASS, msg="All deployed service graph interfaces have vnsRsCIfAttN.", doc_url=doc_url) # Pre-upgrade: fetch both relation object classes for further checks - vnsRsCIfAttNs = icurl("class", "vnsRsCIfAttN.json?rsp-prop-include=config-only") or [] vnsRsCIfAtts = icurl("class", "vnsRsCIfAtt.json?rsp-prop-include=config-only") or [] + vnsRsCIfAttNs = icurl("class", "vnsRsCIfAttN.json?rsp-prop-include=config-only") or [] if sg_data: sg_data.sort(key=lambda r: r[-1]) @@ -6888,15 +6888,15 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * new_dn_keys = set() # New DNs rewritten as old-style keys (for consistency check) for relation_mo in vnsRsCIfAttNs: try: - dn = relation_mo["vnsRsCIfAttN"]["attributes"]["dn"].strip() + relation_dn = relation_mo["vnsRsCIfAttN"]["attributes"]["dn"].strip() except (KeyError, TypeError, AttributeError): continue - if not dn: + if not relation_dn: continue - lif_parent_match = re.search(r"^(uni/tn-[^/]+/lDevVip-[^/]+/lIf-[^/]+)/rscIfAttN-\[", dn) + lif_parent_match = re.search(r"^(uni/tn-[^/]+/lDevVip-[^/]+/lIf-[^/]+)/rscIfAttN-\[", relation_dn) if lif_parent_match: new_lif_dns.add(lif_parent_match.group(1)) - new_dn_keys.add(dn.replace("/rscIfAttN-[", "/rscIfAtt-[", 1)) + new_dn_keys.add(relation_dn.replace("/rscIfAttN-[", "/rscIfAtt-[", 1)) # Secondary coverage check: any deployed LIF not yet covered by a vnsRsCIfAttN data = [] @@ -6906,7 +6906,7 @@ def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, * lif_dn_match = re.search(lif_dn_regex, lif_dn) lif_name = lif_dn_match.group("lif") if lif_dn_match else "" lif_parts = [part for part in lif_name.split("-") if part] - missing_cif = lif_parts[-1] if lif_parts else lif_name + missing_cif = lif_parts[1] if len(lif_parts) > 1 else lif_name data.append([ lif_dn_match.group("tenant") if lif_dn_match else "", lif_dn_match.group("device") if lif_dn_match else "", From 2538b2ba4b1cd26b4216daa20185193ee9d6b340 Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Mon, 6 Jul 2026 07:54:01 +0000 Subject: [PATCH 18/19] Addressed review comments --- aci-preupgrade-validation-script.py | 4 ++-- docs/docs/validations.md | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index e37f522d..647bb34a 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6515,6 +6515,7 @@ def svccore_excessive_data_check(**kwargs): except Exception as e: return Result(result=ERROR, msg="Error occurred while fetching svccore object counts: {}".format(str(e)), doc_url=doc_url) + @check_wrapper(check_title='BGP Timer Policy Already Existing (F0467 bgpProt-policy-already-existing)') def bgpProto_timer_policy_already_existing_check(tversion, cversion, **kwargs): result = FAIL_O @@ -6698,14 +6699,13 @@ def stale_dbgacEpgSummaryTask_check(tversion, **kwargs): if data: result = FAIL_UF - return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) @check_wrapper(check_title="Verifying Cluster Interface Configuration For Deprecated RsCIfAtt Object") def verify_cluster_interface_config_for_rscifatt_use_check(tversion, cversion, **kwargs): result = PASS - doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#check-missing-vnsrscifattn" + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#verifying-cluster-interface-configuration-for-deprecated-rscifatt-object" lif_dn_regex = r"uni/tn-(?P[^/]+)/lDevVip-(?P[^/]+)/lIf-(?P[^/]+)$" ldeviflif_regex = r"^uni/tn-[^/]+/lDevIf-\[(?Puni/tn-[^\]]+/lDevVip-[^\]]+)\]/lDevIfLIf-(?P[^/]+)$" diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 508f5bbf..7d58d2c5 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -242,6 +242,10 @@ 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]: #bgpProto-timer-policy-already-existing +[d35]: #wred-with-affected-fm-models +[d36]: #n9k-c93180yc-fx3-switch-memory-less-than-32gb +[d37]: #stale-dbgacepgsummarytask-objects ## General Check Details From 148d0ac943f15f604008bfc7bd4f034ffac96b25 Mon Sep 17 00:00:00 2001 From: Harinadh Saladi Date: Mon, 6 Jul 2026 10:02:55 +0000 Subject: [PATCH 19/19] Removed the bug link added earlier for this check, as it's added as general check --- docs/docs/validations.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 7d58d2c5..bfe42cef 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -2943,4 +2943,3 @@ Tenant → Services → L4-L7 → Devices → Cluster Interface → Concrete Int [74]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwm42741 [75]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt69100 [76]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwt38698 -[77]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwr51759