From fd64ef6f0a24f79a2e2481b29e12a56e6550da89 Mon Sep 17 00:00:00 2001 From: DHANABALAN SELVARAJ Date: Tue, 30 Jun 2026 10:57:31 +0000 Subject: [PATCH] Added validation check for CSCwt14573 --- aci-preupgrade-validation-script.py | 157 ++++++++++++++++++ docs/docs/validations.md | 24 +++ .../fvCtx_no_stretched_vrf.json | 8 + .../fvCtx_stretched_vrf.json | 18 ++ .../mscGraphXlateCont_missing_xlate.json | 1 + .../mscGraphXlateCont_with_xlate.json | 9 + ...test_vzany_svcgraph_stretched_vrf_check.py | 137 +++++++++++++++ .../vnsGraphInst_with_consumer.json | 33 ++++ .../vzRsAnyToCons_consumer.json | 10 ++ .../vzRsSubjGraphAtt_attached.json | 10 ++ 10 files changed, 407 insertions(+) create mode 100644 tests/checks/vzany_svcgraph_stretched_vrf_check/fvCtx_no_stretched_vrf.json create mode 100644 tests/checks/vzany_svcgraph_stretched_vrf_check/fvCtx_stretched_vrf.json create mode 100644 tests/checks/vzany_svcgraph_stretched_vrf_check/mscGraphXlateCont_missing_xlate.json create mode 100644 tests/checks/vzany_svcgraph_stretched_vrf_check/mscGraphXlateCont_with_xlate.json create mode 100644 tests/checks/vzany_svcgraph_stretched_vrf_check/test_vzany_svcgraph_stretched_vrf_check.py create mode 100644 tests/checks/vzany_svcgraph_stretched_vrf_check/vnsGraphInst_with_consumer.json create mode 100644 tests/checks/vzany_svcgraph_stretched_vrf_check/vzRsAnyToCons_consumer.json create mode 100644 tests/checks/vzany_svcgraph_stretched_vrf_check/vzRsSubjGraphAtt_attached.json diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index ae196700..2bc329e4 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6459,6 +6459,162 @@ def bgpProto_timer_policy_already_existing_check(tversion, cversion, **kwargs): return Result(result=result, headers=headers, data=data, unformatted_headers=unformatted_headers, unformatted_data=unformatted_data, recommended_action=recommended_action, doc_url=doc_url) +@check_wrapper(check_title='vzAny Service Graph on Stretched VRF') +def vzany_svcgraph_stretched_vrf_check(tversion, **kwargs): + result = PASS + headers = ['Tenant', 'VRF', 'Contract', 'Graph', 'Issue'] + data = [] + recommended_action = 'Migrate vzAny service graph configuration to NDO before upgrade using brownfield import. See documentation for detailed migration steps.' + doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#vzany-service-graph-stretched-vrf' + + if not tversion: + return Result(result=MANUAL, msg=TVER_MISSING, doc_url=doc_url) + + if not tversion.newer_than("6.1(3g)"): + return Result(result=PASS, msg="Target version does not trigger CSCwn95571 validation", doc_url=doc_url) + + has_error = False + + try: + graph_insts = icurl('class', 'vnsGraphInst.json?query-target-filter=eq(vnsGraphInst.configSt,"applied")&rsp-subtree=full') + except Exception as e: + return Result(result=ERROR, msg='Error querying applied service graphs: {}'.format(str(e)), doc_url=doc_url) + + if not graph_insts: + return Result(result=PASS, msg="No applied service graphs found", doc_url=doc_url) + + sg_by_contract = {} + for graph_inst_mo in graph_insts: + gi_attrs = graph_inst_mo.get('vnsGraphInst', {}).get('attributes', {}) + gi_dn = gi_attrs.get('dn', '') + contract_dn = gi_attrs.get('ctrctDn', '') + if not contract_dn: + continue + + graph_name_match = re.search(r'-G-\[uni/tn-[^/]+/AbsGraph-([^\]]+)\]', gi_dn) + graph_name = graph_name_match.group(1) if graph_name_match else '' + scope_match = re.search(r'-S-\[(.*?)\]', gi_dn) + scope_dn = scope_match.group(1) if scope_match else '' + + first_node_name = None + for graph_component_mo in graph_inst_mo.get('vnsGraphInst', {}).get('children', []): + if 'vnsTermNodeInst' not in graph_component_mo: + continue + term_attrs = graph_component_mo['vnsTermNodeInst'].get('attributes', {}) + if term_attrs.get('type') != 'consumer': + continue + for term_connection_mo in graph_component_mo['vnsTermNodeInst'].get('children', []): + if 'vnsConnectionInst' not in term_connection_mo: + continue + for conn_rel_mo in term_connection_mo['vnsConnectionInst'].get('children', []): + if 'vnsRsConnectionInstConns' not in conn_rel_mo: + continue + node_tDn = conn_rel_mo['vnsRsConnectionInstConns'].get('attributes', {}).get('tDn', '') + node_match = re.search(r'/NodeInst-([^\]]+)', node_tDn) + if node_match: + first_node_name = node_match.group(1) + break + if first_node_name: + break + if first_node_name: + break + + sg_by_contract[contract_dn] = { + 'gi_dn': gi_dn, + 'graph_name': graph_name, + 'scope_dn': scope_dn, + 'first_node': first_node_name, + } + + if not sg_by_contract: + return Result(result=PASS, msg="No applied service graphs with contracts found", doc_url=doc_url) + + stretched_vrf_dns = set() + try: + fvctx_list = icurl('class', 'fvCtx.json?rsp-subtree=children&rsp-subtree-class=fvSiteAssociated&rsp-subtree-include=required') + for vrf_ctx_mo in fvctx_list: + ctx_dn = vrf_ctx_mo.get('fvCtx', {}).get('attributes', {}).get('dn', '') + for site_assoc_mo in vrf_ctx_mo.get('fvCtx', {}).get('children', []): + if 'fvSiteAssociated' not in site_assoc_mo: + continue + for remote_id_mo in site_assoc_mo['fvSiteAssociated'].get('children', []): + if 'fvRemoteId' in remote_id_mo: + stretched_vrf_dns.add(ctx_dn) + break + except Exception as e: + return Result(result=ERROR, msg='Error querying stretched VRFs: {}'.format(str(e)), doc_url=doc_url) + + if not stretched_vrf_dns: + return Result(result=PASS, msg="No stretched VRFs found", doc_url=doc_url) + + vzany_on_stretched = {} + for rel_class in ('vzRsAnyToCons', 'vzRsAnyToProv'): + try: + rels = icurl('class', '{}.json'.format(rel_class)) + except Exception as e: + has_error = True + data.append(['-', '-', '-', '-', 'Error querying {}: {}'.format(rel_class, str(e))]) + continue + for rel in rels: + rel_attrs = rel.get(rel_class, {}).get('attributes', {}) + contract_dn = rel_attrs.get('tDn', '') + if contract_dn not in sg_by_contract: + continue + rel_dn = rel_attrs.get('dn', '') + vrf_dn_match = re.match(r'(uni/tn-[^/]+/ctx-[^/]+)', rel_dn) + if not vrf_dn_match: + continue + vrf_dn = vrf_dn_match.group(1) + if vrf_dn not in stretched_vrf_dns: + continue + vrf_name_match = re.search(r'ctx-([^/]+)', vrf_dn) + vrf_name = vrf_name_match.group(1) if vrf_name_match else vrf_dn + vzany_on_stretched[contract_dn] = {'vrf_name': vrf_name} + + if not vzany_on_stretched and not has_error: + return Result(result=PASS, msg="No vzAny service graph contracts on stretched VRFs", doc_url=doc_url) + + for contract_dn, vrf_info in vzany_on_stretched.items(): + sg_info = sg_by_contract[contract_dn] + first_node_name = sg_info['first_node'] + if not first_node_name: + continue + + contract_match = re.match(r'uni/tn-([^/]+)/brc-([^/]+)', contract_dn) + if not contract_match: + continue + tenant = contract_match.group(1) + contract = contract_match.group(2) + graph_name = sg_info['graph_name'] + scope_dn = sg_info['scope_dn'] + vrf_name = vrf_info['vrf_name'] + + epg_def_dn = ( + "uni/tn-{}/GraphInst_C-[uni/tn-{}/brc-{}]" + "-G-[uni/tn-{}/AbsGraph-{}]-S-[{}]" + "/NodeInst-{}/LegVNode-0/EPgDef-consumer" + ).format(tenant, tenant, contract, tenant, graph_name, scope_dn, first_node_name) + xlate_dn = "uni/tn-{}/mscGraphXlateCont/epgDefXlate-[{}]".format(tenant, epg_def_dn) + + try: + result_query = icurl('mo', '{}.json'.format(xlate_dn)) + has_xlate = len(result_query) > 0 + except Exception: + has_xlate = False + has_error = True + data.append([tenant, vrf_name, contract, graph_name, 'Error querying vnsEpgDefXlate']) + continue + + if not has_xlate: + data.append([tenant, vrf_name, contract, graph_name, 'Missing vnsEpgDefXlate for 1st node consumer leg']) + + if has_error: + result = ERROR + elif data: + result = FAIL_O + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) + + # ---- Script Execution ----. @@ -6631,6 +6787,7 @@ class CheckManager: n9k_c9408_model_lem_count_check, inband_management_policy_misconfig_check, bgpProto_timer_policy_already_existing_check, + vzany_svcgraph_stretched_vrf_check, ] ssh_checks = [ # General diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 807c5abd..194493a7 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -204,6 +204,7 @@ Items | Defect | This Script [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: [BgpProto timer policy already existing][d34] | CSCwt78235 | :white_check_mark: | :no_entry_sign: +[vzany_svcgraph_stretched_vrf_check][d35] | CSCwt14573 | :white_check_mark: | :no_entry_sign: [d1]: #ep-announce-compatibility [d2]: #eventmgr-db-size-defect-susceptibility @@ -239,6 +240,7 @@ Items | Defect | This Script [d32]: #multi-pod-modular-spine-bootscript-file [d33]: #inband-management-policy-misconfiguration [d34]: #bgpProto-timer-policy-already-existing +[d35]: #vzany-service-graph-stretched-vrf ## General Check Details @@ -2802,6 +2804,27 @@ This check will verify the count of the `svccoreCtrlr` Managed Object and raise This bug [CSCwt78235][71] validates `F0467` faults where `changeSet` contains 'bgpProt-policy-already-existing'. The fault indicates conflicting BGP protocol timer policy under an L3Outs deployed in same vrf under same node. If this fault is not resolved, l3out will not be programmed properly in the leaf after the clean reboot or the upgrade. +### vzAny Service Graph on Stretched VRF + +Due to [CSCwn95571], starting from ACI 6.1(4), a new multisite validation was introduced for service graphs used with vzAny contracts on stretched VRFs. When upgrading to 6.1(4) or later, if a vzAny contract with a service graph is configured locally on the APIC (not through Nexus Dashboard Orchestrator), the service graph will fail to instantiate with faults F0758 and F1690. + +The validation checks whether `vnsEpgDefXlate` translation entries exist for the first node's consumer leg of the service graph. These entries are only created by NDO during template deployment. When the configuration is managed locally on the APIC, these entries are absent, causing the graph rendering to fail. + +This check detects configurations where **all** of the following conditions are true: + +1. The VRF is stretched across multiple sites (has `fvSiteAssociated` with `fvRemoteId` children) +2. vzAny is used as either consumer **or** provider on the stretched VRF +3. The contract has a service graph attached (any type — PBR is **not** required) +4. No `vnsEpgDefXlate` MOs exist for the service graph's first node consumer leg + +!!! note + The fault alone does **not** cause traffic impact for already-deployed graphs. Traffic impact only occurs if the service graph is detached and re-attached to the contract while the fault condition is present. + +!!! note + This applies to **all** service graph types including firewalls with PBR, load balancers without PBR, and any other L4-L7 service devices. + +Recommended action: Migrate the vzAny service graph configuration to NDO before upgrade using brownfield import. NDO 4.2(3e) or later is required for vzAny PBR support on stretched VRFs. This is tracked under [CSCwt14573][72]. + [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 @@ -2875,3 +2898,4 @@ This bug [CSCwt78235][71] validates `F0467` faults where `changeSet` contains 'b [69]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCws84232 [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/CSCwt14573 \ No newline at end of file diff --git a/tests/checks/vzany_svcgraph_stretched_vrf_check/fvCtx_no_stretched_vrf.json b/tests/checks/vzany_svcgraph_stretched_vrf_check/fvCtx_no_stretched_vrf.json new file mode 100644 index 00000000..b210dbc0 --- /dev/null +++ b/tests/checks/vzany_svcgraph_stretched_vrf_check/fvCtx_no_stretched_vrf.json @@ -0,0 +1,8 @@ +[ + { + "fvCtx": { + "attributes": {"dn": "uni/tn-Tenant1/ctx-VRF1", "name": "VRF1"}, + "children": [] + } + } +] diff --git a/tests/checks/vzany_svcgraph_stretched_vrf_check/fvCtx_stretched_vrf.json b/tests/checks/vzany_svcgraph_stretched_vrf_check/fvCtx_stretched_vrf.json new file mode 100644 index 00000000..a12a7203 --- /dev/null +++ b/tests/checks/vzany_svcgraph_stretched_vrf_check/fvCtx_stretched_vrf.json @@ -0,0 +1,18 @@ +[ + { + "fvCtx": { + "attributes": {"dn": "uni/tn-Tenant1/ctx-VRF1", "name": "VRF1"}, + "children": [ + { + "fvSiteAssociated": { + "children": [ + { + "fvRemoteId": {"attributes": {"id": "1"}} + } + ] + } + } + ] + } + } +] diff --git a/tests/checks/vzany_svcgraph_stretched_vrf_check/mscGraphXlateCont_missing_xlate.json b/tests/checks/vzany_svcgraph_stretched_vrf_check/mscGraphXlateCont_missing_xlate.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/tests/checks/vzany_svcgraph_stretched_vrf_check/mscGraphXlateCont_missing_xlate.json @@ -0,0 +1 @@ +[] diff --git a/tests/checks/vzany_svcgraph_stretched_vrf_check/mscGraphXlateCont_with_xlate.json b/tests/checks/vzany_svcgraph_stretched_vrf_check/mscGraphXlateCont_with_xlate.json new file mode 100644 index 00000000..f08685c7 --- /dev/null +++ b/tests/checks/vzany_svcgraph_stretched_vrf_check/mscGraphXlateCont_with_xlate.json @@ -0,0 +1,9 @@ +[ + { + "vnsEpgDefXlate": { + "attributes": { + "dn": "uni/tn-Tenant1/mscGraphXlateCont/epgDefXlate-[...]" + } + } + } +] diff --git a/tests/checks/vzany_svcgraph_stretched_vrf_check/test_vzany_svcgraph_stretched_vrf_check.py b/tests/checks/vzany_svcgraph_stretched_vrf_check/test_vzany_svcgraph_stretched_vrf_check.py new file mode 100644 index 00000000..f33c3f52 --- /dev/null +++ b/tests/checks/vzany_svcgraph_stretched_vrf_check/test_vzany_svcgraph_stretched_vrf_check.py @@ -0,0 +1,137 @@ +import os +import pytest +import logging +import importlib +from helpers.utils import read_data + +script = importlib.import_module("aci-preupgrade-validation-script") +log = logging.getLogger(__name__) +dir = os.path.dirname(os.path.abspath(__file__)) + +test_function = "vzany_svcgraph_stretched_vrf_check" + +# icurl query keys (in execution order) +vnsGraphInst_applied_query = 'vnsGraphInst.json?query-target-filter=eq(vnsGraphInst.configSt,"applied")&rsp-subtree=full' +fvCtx_query = "fvCtx.json?rsp-subtree=children&rsp-subtree-class=fvSiteAssociated&rsp-subtree-include=required" +vzRsAnyToCons_query = "vzRsAnyToCons.json" +vzRsAnyToProv_query = "vzRsAnyToProv.json" + +XLATE_DN = ( + "uni/tn-Tenant1/mscGraphXlateCont/epgDefXlate-[" + "uni/tn-Tenant1/GraphInst_C-[uni/tn-Tenant1/brc-Contract1]" + "-G-[uni/tn-Tenant1/AbsGraph-Graph1]" + "-S-[uni/tn-Tenant1/ctx-VRF1]" + "/NodeInst-FirstNode/LegVNode-0/EPgDef-consumer].json" +) + + +@pytest.mark.parametrize( + "icurl_outputs, cversion, tversion, expected_result, expected_data, expected_msg", + [ + # Target version missing -> MANUAL + ( + {}, + "6.0(1a)", + None, + script.MANUAL, + [], + None, + ), + # Target version older than 6.1(4) -> PASS (version gate, no API calls) + ( + {}, + "6.0(1a)", + "6.1(3d)", + script.PASS, + [], + None, + ), + # No applied service graphs -> PASS + ( + { + vnsGraphInst_applied_query: [], + }, + "6.0(1a)", + "6.1(4a)", + script.PASS, + [], + None, + ), + # Applied SGs but no stretched VRFs -> PASS + ( + { + vnsGraphInst_applied_query: read_data(dir, "vnsGraphInst_with_consumer.json"), + fvCtx_query: read_data(dir, "fvCtx_no_stretched_vrf.json"), + }, + "6.0(1a)", + "6.1(4a)", + script.PASS, + [], + None, + ), + # Applied SGs + stretched VRFs but contract not in vzAny -> PASS + ( + { + vnsGraphInst_applied_query: read_data(dir, "vnsGraphInst_with_consumer.json"), + fvCtx_query: read_data(dir, "fvCtx_stretched_vrf.json"), + vzRsAnyToCons_query: [], + vzRsAnyToProv_query: [], + }, + "6.0(1a)", + "6.1(4a)", + script.PASS, + [], + None, + ), + # All conditions met, xlate present -> PASS + ( + { + vnsGraphInst_applied_query: read_data(dir, "vnsGraphInst_with_consumer.json"), + fvCtx_query: read_data(dir, "fvCtx_stretched_vrf.json"), + vzRsAnyToCons_query: read_data(dir, "vzRsAnyToCons_consumer.json"), + vzRsAnyToProv_query: [], + XLATE_DN: read_data(dir, "mscGraphXlateCont_with_xlate.json"), + }, + "6.0(1a)", + "6.1(4a)", + script.PASS, + [], + None, + ), + # All conditions met, xlate missing -> FAIL_O + ( + { + vnsGraphInst_applied_query: read_data(dir, "vnsGraphInst_with_consumer.json"), + fvCtx_query: read_data(dir, "fvCtx_stretched_vrf.json"), + vzRsAnyToCons_query: read_data(dir, "vzRsAnyToCons_consumer.json"), + vzRsAnyToProv_query: [], + XLATE_DN: [], + }, + "6.0(1a)", + "6.1(4a)", + script.FAIL_O, + [["Tenant1", "VRF1", "Contract1", "Graph1", "Missing vnsEpgDefXlate for 1st node consumer leg"]], + None, + ), + # Error querying applied service graphs -> ERROR + ( + None, # None signals exception on first icurl call + "6.0(1a)", + "6.1(4a)", + script.ERROR, + None, + None, + ), + ], +) +def test_vzany_svcgraph_stretched_vrf_check(run_check, mock_icurl, cversion, tversion, expected_result, expected_data, expected_msg): + """Test vzany_svcgraph_stretched_vrf_check with various scenarios""" + result = run_check( + tversion=script.AciVersion(tversion) if tversion else None, + cversion=script.AciVersion(cversion), + ) + assert result.result == expected_result + if expected_data is not None: + assert result.data == expected_data + if expected_msg is not None: + assert result.msg == expected_msg diff --git a/tests/checks/vzany_svcgraph_stretched_vrf_check/vnsGraphInst_with_consumer.json b/tests/checks/vzany_svcgraph_stretched_vrf_check/vnsGraphInst_with_consumer.json new file mode 100644 index 00000000..01cd44be --- /dev/null +++ b/tests/checks/vzany_svcgraph_stretched_vrf_check/vnsGraphInst_with_consumer.json @@ -0,0 +1,33 @@ +[ + { + "vnsGraphInst": { + "attributes": { + "dn": "uni/tn-Tenant1/GraphInst_C-[uni/tn-Tenant1/brc-Contract1]-G-[uni/tn-Tenant1/AbsGraph-Graph1]-S-[uni/tn-Tenant1/ctx-VRF1]", + "configSt": "applied", + "ctrctDn": "uni/tn-Tenant1/brc-Contract1" + }, + "children": [ + { + "vnsTermNodeInst": { + "attributes": {"type": "consumer"}, + "children": [ + { + "vnsConnectionInst": { + "children": [ + { + "vnsRsConnectionInstConns": { + "attributes": { + "tDn": "uni/tn-Tenant1/GraphInst/NodeInst-FirstNode" + } + } + } + ] + } + } + ] + } + } + ] + } + } +] diff --git a/tests/checks/vzany_svcgraph_stretched_vrf_check/vzRsAnyToCons_consumer.json b/tests/checks/vzany_svcgraph_stretched_vrf_check/vzRsAnyToCons_consumer.json new file mode 100644 index 00000000..94595447 --- /dev/null +++ b/tests/checks/vzany_svcgraph_stretched_vrf_check/vzRsAnyToCons_consumer.json @@ -0,0 +1,10 @@ +[ + { + "vzRsAnyToCons": { + "attributes": { + "dn": "uni/tn-Tenant1/ctx-VRF1/rsanyToCons-[uni/tn-Tenant1/brc-Contract1]", + "tDn": "uni/tn-Tenant1/brc-Contract1" + } + } + } +] diff --git a/tests/checks/vzany_svcgraph_stretched_vrf_check/vzRsSubjGraphAtt_attached.json b/tests/checks/vzany_svcgraph_stretched_vrf_check/vzRsSubjGraphAtt_attached.json new file mode 100644 index 00000000..820b3379 --- /dev/null +++ b/tests/checks/vzany_svcgraph_stretched_vrf_check/vzRsSubjGraphAtt_attached.json @@ -0,0 +1,10 @@ +[ + { + "vzRsSubjGraphAtt": { + "attributes": { + "dn": "uni/tn-Tenant1/brc-Contract1/subj-Subject1/rsSubjGraphAtt", + "tDn": "uni/tn-Tenant1/GraphInst/node" + } + } + } +]