diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 5e79f56..647bb34 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 @@ -6702,6 +6702,256 @@ 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="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/#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[^/]+)$" + 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) + + 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" + + # ── Step 1: Collect deployed service-graph LIF DNs ────────────────────── + + # Build (contract_name, graph_name) keys from applied vnsGraphInst objects + graph_keys = set() + for entry in icurl("class", "vnsGraphInst.json?rsp-prop-include=config-only") or []: + try: + 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-([^/]+)$", 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))) + + lif_dns = set() + lif_source_tenants = {} # lif_dn -> set(contract tenants) for implicit-object detection + dev_source_tenants = {} # device-prefix DN -> set(contract tenants) + + ldev_ctx_query = ( + "vnsLDevCtx.json?rsp-prop-include=config-only" + "&rsp-subtree=full" + "&rsp-subtree-class=vnsLIfCtx,vnsRsLIfCtxToLIf" + "&rsp-subtree-include=required" + ) + for entry in icurl("class", ldev_ctx_query) or []: + try: + 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_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] + 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 = [ldev_ctx_mo] + while stack: + current_ctx = stack.pop() + for child in current_ctx.get("children", []) or []: + if child.get("vnsLIfCtx"): + stack.append(child["vnsLIfCtx"]) + lif_relation = child.get("vnsRsLIfCtxToLIf") + if not lif_relation: + continue + try: + target_class = lif_relation["attributes"].get("tCl", "") + target_dn = lif_relation["attributes"]["tDn"].strip() + except (KeyError, TypeError, AttributeError): + continue + if not target_dn: + continue + + 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 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(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: + 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 []: + try: + lif_dn = entry["vnsLIf"]["attributes"]["dn"].strip() + except (KeyError, TypeError, AttributeError): + continue + lif_dn_match = re.search(lif_dn_regex, lif_dn) + if not lif_dn_match: + continue + 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): + lif_source_tenants.setdefault(lif_dn, set()).update(dev_source_tenants[dev_dn]) + + 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(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(subtree_mo, dict) and subtree_mo.get("vnsRsCIfAttN") for subtree_mo in lif_subtree): + continue + 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 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 "", + 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 + + # ── Step 3: Return results ─────────────────────────────────────────────── + + if post_upgrade: + if 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=[[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) + + # Pre-upgrade: fetch both relation object classes for further checks + 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]) + 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) + + if not vnsRsCIfAtts and not vnsRsCIfAttNs: + 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) + + if not vnsRsCIfAtts: + return Result(result=PASS, msg="No user-configured vnsRsCIfAtt payload found.", doc_url=doc_url) + + # 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 relation_mo in vnsRsCIfAttNs: + try: + relation_dn = relation_mo["vnsRsCIfAttN"]["attributes"]["dn"].strip() + except (KeyError, TypeError, AttributeError): + continue + if not relation_dn: + continue + 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(relation_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 + 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 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 "", + 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 old_relation_mo in vnsRsCIfAtts: + try: + old_dn = old_relation_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 = 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 + old_relation_match = re.search( + r"uni/tn-(?P[^/]+)/lDevVip-(?P[^/]+)/lIf-(?P[^/]+)/" + r"rscIfAtt-\[.*?/cIf-\[(?P[^\]]+)\]\]", + old_dn, + ) + data.append([ + 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, + ]) + + data.sort(key=lambda r: r[-1]) + if data: + result = FAIL_O + + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) + + # ---- Script Execution ---- @@ -6794,6 +7044,7 @@ class CheckManager: fabric_link_redundancy_check, apic_downgrade_compat_warning_check, svccore_excessive_data_check, + verify_cluster_interface_config_for_rscifatt_use_check, # Faults apic_disk_space_faults_check, @@ -6877,7 +7128,6 @@ class CheckManager: wred_affected_model_check, n9k_c93180yc_fx3_switch_memory_check, stale_dbgacEpgSummaryTask_check, - ] ssh_checks = [ # General diff --git a/docs/docs/validations.md b/docs/docs/validations.md index f788681..bfe42ce 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -39,6 +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: +[Verifying Cluster Interface Configuration For Deprecated RsCIfAtt Object][g22] | :white_check_mark: | :no_entry_sign: [g1]: #compatibility-target-aci-version [g2]: #compatibility-cimc-version @@ -61,6 +62,7 @@ Items | This Script [g19]: #apic-downgrade-compatibility-when-crossing-62-release [g20]: #supported-hardware-compatibility [g21]: #svccore-excessive-data-check +[g22]: #verifying-cluster-interface-configuration-for-deprecated-rscifatt-object ### Fault Checks Items | Faults | This Script | APIC built-in @@ -2782,7 +2784,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. @@ -2845,6 +2846,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 @@ -2923,4 +2943,3 @@ 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 - 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 new file mode 100644 index 0000000..a39f0a1 --- /dev/null +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/test_verify_cluster_interface_config_for_rscifatt_use_check.py @@ -0,0 +1,336 @@ +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 = "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?rsp-prop-include=config-only' +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; if service graph is unconfigured, return PASS + ( + { + 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.PASS, + [], + ), + # Both vnsRsCIfAtt and vnsRsCIfAttN are missing but service graph is unconfigured -> PASS + ( + { + 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.PASS, + [], + ), + # Both vnsRsCIfAtt and vnsRsCIfAttN are missing while vnsLIf exists, but service graph is unconfigured -> PASS + ( + { + 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.PASS, + [], + ), + # 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, + [], + ), + # One vnsRsCIfAtt relation (cons) missing in vnsRsCIfAttN, but service graph unconfigured -> PASS + ( + { + 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.PASS, + [], + ), + # vnsRsCIfAttN is empty and old relations exist, but service graph unconfigured -> PASS + ( + { + vnsRsCIfAtt_api: read_data(dir, "vnsRsCIfAtt_match.json"), + vnsRsCIfAttN_api: [], + vnsGraphInst_api: [], + vnsLDevCtx_all_api: [], + }, + "6.1(5e)", + script.PASS, + [], + ), + # 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", + ], + ], + ), + # 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"), + 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.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", + ], + ], + ), + ], +) +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 + assert result.data == expected_data diff --git a/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsGraphInst_applied_single.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsGraphInst_applied_single.json new file mode 100644 index 0000000..80fe130 --- /dev/null +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_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/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLDevIfLIf_missing_rscifattn.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLDevIfLIf_missing_rscifattn.json new file mode 100644 index 0000000..691fd7e --- /dev/null +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_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/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json new file mode 100644 index 0000000..9145c08 --- /dev/null +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl.json @@ -0,0 +1,29 @@ +[ + { + "vnsLDevCtx": { + "attributes": { + "dn": "uni/tn-user/ldevCtx-c-common-epg-epg-g-test-n-N1", + "ctrctNameOrLbl": "common-epg-epg", + "graphNameOrLbl": "test" + }, + "children": [ + { + "vnsLIfCtx": { + "attributes": { + "dn": "uni/tn-user/ldevCtx-c-common-epg-epg-g-test-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/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl_imported.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLDevIfLIf_missing_tcl_imported.json new file mode 100644 index 0000000..bdb5b3f --- /dev/null +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_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" + } + } + } + ] + } + } + ] + } + } +] diff --git a/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLIf_missing_rscifattn.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLDevCtx_vnsLIf_missing_rscifattn.json new file mode 100644 index 0000000..96183d3 --- /dev/null +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_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/verify_cluster_interface_config_for_rscifatt_use_check/vnsLIf_common_device_both.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLIf_common_device_both.json new file mode 100644 index 0000000..8944e08 --- /dev/null +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_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/verify_cluster_interface_config_for_rscifatt_use_check/vnsLIf_only.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLIf_only.json new file mode 100644 index 0000000..0eb80a8 --- /dev/null +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_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" + } + } + } +] diff --git a/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLIf_subtree_parent_only.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLIf_subtree_parent_only.json new file mode 100644 index 0000000..65b7531 --- /dev/null +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsLIf_subtree_parent_only.json @@ -0,0 +1,9 @@ +[ + { + "vnsLIf": { + "attributes": { + "dn": "uni/tn-CSCwj49418/lDevVip-test/lIf-intf-prov" + } + } + } +] diff --git a/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAttN_match.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAttN_match.json new file mode 100644 index 0000000..2344730 --- /dev/null +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_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/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAttN_missing_cons.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAttN_missing_cons.json new file mode 100644 index 0000000..25de6eb --- /dev/null +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_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/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAtt_empty.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAtt_empty.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAtt_empty.json @@ -0,0 +1 @@ +[] diff --git a/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAtt_match.json b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_check/vnsRsCIfAtt_match.json new file mode 100644 index 0000000..a9b0696 --- /dev/null +++ b/tests/checks/verify_cluster_interface_config_for_rscifatt_use_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]" + } + } + } +]