diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 094f0e7..1b5c90e 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6397,6 +6397,187 @@ def apic_downgrade_compat_warning_check(cversion, tversion, **kwargs): return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) +@check_wrapper(check_title='Shared Services Providers with Preferred Group enabled') +def pg_and_shared_svc_contract_check(cversion, tversion, **kwargs): + result= PASS + headers = ["Shared Service Contract", "Provider in Preferred Group", "PcTag", "Affected Consumer"] + data = [] + recommended_action = ( + "Before upgrading, remove each listed provider from the Preferred Group, " + "stop it from providing the listed shared-service contract, or remove the " + "unsupported L3Out/vzAny consumer relationship. Re-deploy the policy and " + "confirm the contract and Preferred Group configuration deploy successfully. " + "On releases that enforce this restriction, verify that F0467 or F4684 clears." + ) + doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#preferred-group-shared-service-provider' + + if not tversion: + return Result(result=MANUAL, msg=TVER_MISSING) + # Only releases 4.2 and later are in scope for this validation. + if tversion.older_than("4.2(1a)"): + return Result(result=NA) + shrd_contracts_api = 'vzBrCP.json' + shrd_contracts_api += '?query-target-filter=or(eq(vzBrCP.scope,"global"),eq(vzBrCP.scope,"tenant"))' + shrd_contracts = icurl('class', shrd_contracts_api) + if not shrd_contracts: + return Result(result=NA) + shared_contract_scopes = {} + for shrd_contract in shrd_contracts: + contract_attributes = shrd_contract["vzBrCP"]["attributes"] + shared_contract_scopes[contract_attributes["dn"]] = contract_attributes["scope"] + + glbl_epgs_api = 'fvAEPg.json' + glbl_epgs_api += '?query-target-filter=and(le(fvAEPg.pcTag,"16385"),ge(fvAEPg.pcTag,"17"),eq(fvAEPg.prefGrMemb,"include"))' + glbl_epgs_api += '&rsp-subtree=children&rsp-subtree-class=fvRsProv' + glbl_epgs = icurl('class', glbl_epgs_api) + + glbl_ext_epgs_api = 'l3extInstP.json' + glbl_ext_epgs_api += '?query-target-filter=and(le(l3extInstP.pcTag,"16385"),ge(l3extInstP.pcTag,"17"),eq(l3extInstP.prefGrMemb,"include"))' + glbl_ext_epgs_api += '&rsp-subtree=children&rsp-subtree-class=fvRsProv' + glbl_ext_epgs = icurl('class', glbl_ext_epgs_api) + + shared_service_providers = [] + for provider_class, providers in (("fvAEPg", glbl_epgs), ("l3extInstP", glbl_ext_epgs)): + for provider_mo in providers: + provider = provider_mo[provider_class] + provider_attributes = provider["attributes"] + for prov_contract in provider.get("children") or []: + contract = prov_contract["fvRsProv"]["attributes"]["tDn"] + if contract in shared_contract_scopes: + shared_service_providers.append((contract, provider_attributes)) + + if shared_service_providers: + providers_by_dn = {} + for contract, provider_attributes in shared_service_providers: + providers_by_dn.setdefault(provider_attributes["dn"], []).append( + (contract, provider_attributes) + ) + + ctx_defs = icurl('class', 'fvCtxDef.json') + ctx_def_by_scope = {} + for ctx_def in ctx_defs: + ctx_attributes = ctx_def["fvCtxDef"]["attributes"] + ctx_def_by_scope[ctx_attributes["scope"]] = ctx_attributes["dn"] + + provider_relationships_api = 'vzFromEPg.json' + provider_relationships_api += '?query-target-filter=and(eq(vzFromEPg.membType,"prov"),' + provider_relationships_api += 'le(vzFromEPg.pcTag,"16385"),ge(vzFromEPg.pcTag,"17"))' + provider_relationships_api += '&rsp-subtree=children&rsp-subtree-class=vzToEPg' + provider_relationships = icurl('class', provider_relationships_api) + cross_context_relationships = [] + relationship_errors = [] + + def tenant_dn(dn): + dn_parts = dn.split("/", 2) + if len(dn_parts) >= 2 and dn_parts[0] == "uni" and dn_parts[1].startswith("tn-"): + return "/".join(dn_parts[:2]) + return None + + for provider_relationship in provider_relationships: + from_epg = provider_relationship["vzFromEPg"] + from_attributes = from_epg["attributes"] + provider_dn = from_attributes["epgDn"] + provider_contracts = providers_by_dn.get(provider_dn, []) + if not provider_contracts: + continue + + matching_provider_contracts = [ + provider_contract + for provider_contract in provider_contracts + if from_attributes["dn"].startswith( + "cdef-[{}]/".format(provider_contract[0]) + ) + ] + if not matching_provider_contracts: + continue + + to_epgs = from_epg.get("children") or [] + if not to_epgs: + continue + + provider_ctx_def_dn = ctx_def_by_scope.get(from_attributes["scopeId"]) + if not provider_ctx_def_dn: + relationship_errors.append([ + from_attributes["dn"], + "No fvCtxDef found for scopeId {}".format(from_attributes["scopeId"]) + ]) + continue + + for contract, provider_attributes in matching_provider_contracts: + for to_epg_mo in to_epgs: + if "vzToEPg" not in to_epg_mo: + continue + consumer_attributes = to_epg_mo["vzToEPg"]["attributes"] + consumer_dn = consumer_attributes["epgDn"] + consumer_ctx_def_dn = consumer_attributes.get("ctxDefDn") + if not consumer_ctx_def_dn: + relationship_errors.append([ + consumer_attributes["dn"], + "vzToEPg.ctxDefDn is empty" + ]) + continue + + if shared_contract_scopes[contract] == "tenant": + contract_tenant = tenant_dn(contract) + if ( + contract_tenant != tenant_dn(provider_dn) + or contract_tenant != tenant_dn(consumer_dn) + ): + continue + + if provider_ctx_def_dn != consumer_ctx_def_dn: + cross_context_relationships.append( + (contract, provider_attributes, consumer_dn) + ) + + broad_provider_check = tversion.older_than("6.0(1g)") + if broad_provider_check: + affected_relationships = cross_context_relationships + elif cross_context_relationships: + affected_relationships = [ + relationship + for relationship in cross_context_relationships + if "/instP-" in relationship[2] + or relationship[2].endswith("/any") + ] + else: + affected_relationships = [] + + reported_relationships = set() + for contract, provider_attributes, consumer_dn in affected_relationships: + result_row = ( + contract, + provider_attributes["dn"], + provider_attributes["pcTag"], + consumer_dn + ) + if result_row not in reported_relationships: + reported_relationships.add(result_row) + data.append(list(result_row)) + + if relationship_errors: + return Result( + result=ERROR, + msg="Unable to resolve context for one or more derived contract relationships", + headers=["Derived Relationship", "Context Resolution Error"], + data=relationship_errors, + unformatted_headers=headers, + unformatted_data=data, + recommended_action=( + "Retry the check. Review any confirmed affected relationships " + "shown in the failure details. If context resolution continues " + "to fail, contact Cisco TAC with the listed derived relationship " + "DNs." + ), + doc_url=doc_url + ) + + if data: + result = FAIL_O + + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) + + @check_wrapper(check_title='Auto Firmware Update on Switch Discovery') def auto_firmware_update_on_switch_check(cversion, tversion, **kwargs): result = PASS @@ -7131,6 +7312,7 @@ class CheckManager: service_bd_forceful_routing_check, ave_eol_check, consumer_vzany_shared_services_check, + pg_and_shared_svc_contract_check, # Bugs ep_announce_check, diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 717c175..ead94e1 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -141,6 +141,7 @@ Items | Faults | This Script [Service Graph BD Forceful Routing][c22] | :white_check_mark: | :no_entry_sign: [AVE End-of-life][c23] | :white_check_mark: | :no_entry_sign: [Shared Service with vzAny Consumer][c24] | :white_check_mark: | :no_entry_sign: +[Preferred Group Shared Service Provider][c25] | :white_check_mark: | :no_entry_sign: [c1]: #vpc-paired-leaf-switches [c2]: #overlapping-vlan-pool @@ -166,6 +167,7 @@ Items | Faults | This Script [c22]: #service-graph-bd-forceful-routing [c23]: #ave-end-of-life [c24]: #shared-service-with-vzany-consumer +[c25]: #preferred-group-shared-service-provider ### Defect Condition Checks @@ -2372,6 +2374,19 @@ See [Inter-VRF contract with vzAny as the consumer][60] in Cisco ACI Contract Gu See [Enable Policy Compression in Cisco ACI Contract Guide][61] for details about Policy Compression. +### Preferred Group Shared Service Provider + +ACI 4.2 and later configurations can be affected by CSCvm63145 and CSCvv51121 when a Preferred Group member provides a tenant- or global-scope shared-service contract to a consumer in another VRF. + +The script reports only materialized, cross-VRF provider-to-consumer relationships represented by `vzFromEPg` and `vzToEPg`. A configured provider without such a relationship is not reported. Tenant-scope contracts are considered only when the contract, provider, and consumer belong to the same tenant. Shared/global pcTags `17` through `16385` are treated as fabric-wide identities; VRF separation is determined independently from the context-definition DNs. + +Before 6.0(1g), any consumer class in a materialized cross-VRF relationship can be affected. Depending on the release, the forwarding risk can be silent or the contract can be rejected with F0467 and `invalid-contract-config: Shared service provider cannot be in a Preferred Group`. + +Starting with 6.0(1g), ordinary EPG-to-EPG shared service is allowed. The unsupported condition remains only when the Preferred Group provider has a materialized relationship with an L3Out or `vzAny` consumer in another VRF. Same-VRF L3Out and `vzAny` relationships are not reported. Starting with 6.1(3f), this condition may be reported through F4684. + +Before upgrading, use the provider and consumer DNs shown in the result to remove the provider from the Preferred Group, stop it from providing the shared-service contract, or remove the unsupported relationship. See the [ACI Policy Model][78] for additional background. + + ## Defect Check Details ### EP Announce Compatibility @@ -2947,3 +2962,4 @@ To avoid this issue, modify the user VLAN pool ranges so that the InfraVLAN does [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/CSCwt58626 +[78]: https://www.cisco.com/c/en/us/td/docs/switches/datacenter/aci/apic/sw/5-x/aci-fundamentals/cisco-aci-fundamentals-50x/m_policy-model.html#concept_tds_vcc_fy diff --git a/tests/checks/pg_and_shared_svc_contract_check/global_pg_fvAEPg.json b/tests/checks/pg_and_shared_svc_contract_check/global_pg_fvAEPg.json new file mode 100644 index 0000000..a7af453 --- /dev/null +++ b/tests/checks/pg_and_shared_svc_contract_check/global_pg_fvAEPg.json @@ -0,0 +1,40 @@ +[ +{"fvAEPg": +{"attributes": +{"annotation":"","childAction":"","configIssues":"","configSt":"applied","descr":"","dn":"uni/tn-common/ap-apptest/epg-epg1","exceptionTag":"","extMngdBy":"","floodOnEncap":"disabled","fwdCtrl":"","hasMcastSource":"no","isAttrBasedEPg":"no","isSharedSrvMsiteEPg":"no","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-08-07T18:05:49.910+00:00","monPolDn":"uni/tn-common/monepg-default","name":"epg1","nameAlias":"","pcEnfPref":"unenforced","pcTag":"5555","pcTagAllocSrc":"idmanager","prefGrMemb":"include","prio":"unspecified","scope":"2261001","shutdown":"no","status":"","triggerSt":"triggerable","txId":"1729382256913953811","uid":"15374","userdom":":all:"},"children":[ +{"fvRsProv": +{"attributes": +{"accessPrivilege":"USER","annotation":"","childAction":"","ctrctUpd":"ctrct","extMngdBy":"","forceResolve":"yes","intent":"install","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-08-07T18:05:49.910+00:00","monPolDn":"uni/tn-common/monepg-default","prio":"unspecified","rType":"mo","rn":"rsprov-AD_C","state":"formed","stateQual":"none","status":"","tCl":"vzBrCP","tContextDn":"","tDn":"uni/tn-common/brc-AD_C","tRn":"brc-AD_C","tType":"name","tnVzBrCPName":"AD_C","triggerSt":"triggerable","uid":"15374","updateCollection":"no","userdom":":all:"}}}]}}, +{"fvAEPg": +{"attributes": +{"annotation":"orchestrator:msc-shadow:no","childAction":"","configIssues":"","configSt":"applied","descr":"","dn":"uni/tn-AJ-PROD/ap-PTC_AP1/epg-PTC_EPG4","exceptionTag":"","extMngdBy":"","floodOnEncap":"disabled","fwdCtrl":"","hasMcastSource":"no","isAttrBasedEPg":"no","isSharedSrvMsiteEPg":"yes","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-08-06T23:29:28.934+00:00","monPolDn":"uni/tn-common/monepg-default","name":"PTC_EPG4","nameAlias":"","pcEnfPref":"unenforced","pcTag":"44","pcTagAllocSrc":"idmanager","prefGrMemb":"include","prio":"unspecified","scope":"2555906","shutdown":"no","status":"","triggerSt":"triggerable","txId":"7493989779944511091","uid":"0","userdom":"all"},"children":[ +{"fvRsProv": +{"attributes": +{"accessPrivilege":"USER","annotation":"orchestrator:msc-shadow:no","childAction":"","ctrctUpd":"ctrct","extMngdBy":"","forceResolve":"yes","intent":"install","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-08-06T23:29:28.934+00:00","monPolDn":"uni/tn-common/monepg-default","prio":"unspecified","rType":"mo","rn":"rsprov-common_contract","state":"formed","stateQual":"none","status":"","tCl":"vzBrCP","tContextDn":"","tDn":"uni/tn-common/brc-common_contract","tRn":"brc-common_contract","tType":"name","tnVzBrCPName":"common_contract","triggerSt":"triggerable","uid":"0","updateCollection":"no","userdom":"all"}}}]}}, +{"fvAEPg": +{"attributes": +{"annotation":"","childAction":"","configIssues":"","configSt":"applied","descr":"","dn":"uni/tn-vavictor_TenantA/ap-eApp_AP/epg-App_EPG","exceptionTag":"","extMngdBy":"","floodOnEncap":"disabled","fwdCtrl":"","hasMcastSource":"no","isAttrBasedEPg":"no","isSharedSrvMsiteEPg":"no","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-07-20T00:45:30.670+00:00","monPolDn":"uni/tn-common/monepg-default","name":"App_EPG","nameAlias":"","pcEnfPref":"unenforced","pcTag":"10932","pcTagAllocSrc":"idmanager","prefGrMemb":"include","prio":"level3","scope":"2818049","shutdown":"no","status":"","triggerSt":"triggerable","txId":"15564440312192537861","uid":"15374","userdom":":all:"},"children":[ +{"fvRsProv": +{"attributes": +{"accessPrivilege":"USER","annotation":"","childAction":"","ctrctUpd":"ctrct","extMngdBy":"","forceResolve":"yes","intent":"install","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-07-20T00:45:30.670+00:00","monPolDn":"uni/tn-common/monepg-default","prio":"unspecified","rType":"mo","rn":"rsprov-BackEnd_Ctrt","state":"formed","stateQual":"none","status":"","tCl":"vzBrCP","tContextDn":"","tDn":"uni/tn-vavictor_TenantA/brc-BackEnd_Ctrt","tRn":"brc-BackEnd_Ctrt","tType":"name","tnVzBrCPName":"BackEnd_Ctrt","triggerSt":"triggerable","uid":"13299","updateCollection":"no","userdom":":"}}}, +{"fvRsProv": +{"attributes": +{"accessPrivilege":"USER","annotation":"","childAction":"","ctrctUpd":"ctrct","extMngdBy":"","forceResolve":"yes","intent":"install","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-07-20T00:45:30.670+00:00","monPolDn":"uni/tn-common/monepg-default","prio":"unspecified","rType":"mo","rn":"rsprov-L3Out_Ctrt","state":"formed","stateQual":"none","status":"","tCl":"vzBrCP","tContextDn":"","tDn":"uni/tn-vavictor_TenantA/brc-L3Out_Ctrt","tRn":"brc-L3Out_Ctrt","tType":"name","tnVzBrCPName":"L3Out_Ctrt","triggerSt":"triggerable","uid":"15374","updateCollection":"no","userdom":":all:"}}}, +{"fvRsProv": +{"attributes": +{"accessPrivilege":"USER","annotation":"","childAction":"","ctrctUpd":"ctrct","extMngdBy":"","forceResolve":"yes","intent":"install","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-07-20T00:45:30.670+00:00","monPolDn":"uni/tn-common/monepg-default","prio":"unspecified","rType":"mo","rn":"rsprov-FrontEnd_Ctrt","state":"formed","stateQual":"none","status":"","tCl":"vzBrCP","tContextDn":"","tDn":"uni/tn-vavictor_TenantA/brc-FrontEnd_Ctrt","tRn":"brc-FrontEnd_Ctrt","tType":"name","tnVzBrCPName":"FrontEnd_Ctrt","triggerSt":"triggerable","uid":"15374","updateCollection":"no","userdom":":all:"}}}, +{"fvRsProv": +{"attributes": +{"accessPrivilege":"USER","annotation":"","childAction":"","ctrctUpd":"ctrct","extMngdBy":"","forceResolve":"yes","intent":"install","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-07-20T00:45:30.670+00:00","monPolDn":"uni/tn-common/monepg-default","prio":"unspecified","rType":"mo","rn":"rsprov-vavictor_from_Scan","state":"formed","stateQual":"none","status":"","tCl":"vzBrCP","tContextDn":"","tDn":"uni/tn-common/brc-vavictor_from_Scan","tRn":"brc-vavictor_from_Scan","tType":"name","tnVzBrCPName":"vavictor_from_Scan","triggerSt":"triggerable","uid":"15374","updateCollection":"no","userdom":":all:"}}}]}}, +{"fvAEPg": +{"attributes": +{"annotation":"","childAction":"","configIssues":"","configSt":"applied","descr":"","dn":"uni/tn-vavictor_TenantA/ap-eApp_AP/epg-Web_EPG","exceptionTag":"","extMngdBy":"","floodOnEncap":"disabled","fwdCtrl":"","hasMcastSource":"no","isAttrBasedEPg":"no","isSharedSrvMsiteEPg":"no","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-04-20T20:22:26.475+00:00","monPolDn":"uni/tn-common/monepg-default","name":"Web_EPG","nameAlias":"","pcEnfPref":"unenforced","pcTag":"5476","pcTagAllocSrc":"idmanager","prefGrMemb":"include","prio":"level3","scope":"2818049","shutdown":"no","status":"","triggerSt":"triggerable","txId":"15564440312192537861","uid":"15374","userdom":":all:"},"children":[ +{"fvRsProv": +{"attributes": +{"accessPrivilege":"USER","annotation":"","childAction":"","ctrctUpd":"ctrct","extMngdBy":"","forceResolve":"yes","intent":"install","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-04-20T20:22:26.475+00:00","monPolDn":"uni/tn-common/monepg-default","prio":"unspecified","rType":"mo","rn":"rsprov-EPG_2_uSeg_Ctrt","state":"formed","stateQual":"none","status":"","tCl":"vzBrCP","tContextDn":"","tDn":"uni/tn-vavictor_TenantA/brc-EPG_2_uSeg_Ctrt","tRn":"brc-EPG_2_uSeg_Ctrt","tType":"name","tnVzBrCPName":"EPG_2_uSeg_Ctrt","triggerSt":"triggerable","uid":"15374","updateCollection":"no","userdom":":all:"}}}, +{"fvRsProv": +{"attributes": +{"accessPrivilege":"USER","annotation":"","childAction":"","ctrctUpd":"ctrct","extMngdBy":"","forceResolve":"yes","intent":"install","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-04-20T20:22:26.475+00:00","monPolDn":"uni/tn-common/monepg-default","prio":"unspecified","rType":"mo","rn":"rsprov-L3Out_Ctrt","state":"formed","stateQual":"none","status":"","tCl":"vzBrCP","tContextDn":"","tDn":"uni/tn-vavictor_TenantA/brc-L3Out_Ctrt","tRn":"brc-L3Out_Ctrt","tType":"name","tnVzBrCPName":"L3Out_Ctrt","triggerSt":"triggerable","uid":"15374","updateCollection":"no","userdom":":all:"}}}, +{"fvRsProv": +{"attributes": +{"accessPrivilege":"USER","annotation":"","childAction":"","ctrctUpd":"ctrct","extMngdBy":"","forceResolve":"yes","intent":"install","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-04-20T21:41:40.628+00:00","monPolDn":"uni/tn-common/monepg-default","prio":"unspecified","rType":"mo","rn":"rsprov-vavictor_from_Scan","state":"formed","stateQual":"none","status":"","tCl":"vzBrCP","tContextDn":"","tDn":"uni/tn-common/brc-vavictor_from_Scan","tRn":"brc-vavictor_from_Scan","tType":"name","tnVzBrCPName":"vavictor_from_Scan","triggerSt":"triggerable","uid":"15374","updateCollection":"no","userdom":":all:"}}}]}}] \ No newline at end of file diff --git a/tests/checks/pg_and_shared_svc_contract_check/global_pg_l3extInstP.json b/tests/checks/pg_and_shared_svc_contract_check/global_pg_l3extInstP.json new file mode 100644 index 0000000..98ffc9f --- /dev/null +++ b/tests/checks/pg_and_shared_svc_contract_check/global_pg_l3extInstP.json @@ -0,0 +1,3 @@ +[ +{"l3extInstP":{"attributes":{"annotation":"","childAction":"","configIssues":"","configSt":"applied","descr":"","dn":"uni/tn-common/out-test-L3Out/instP-testExtEPG","exceptionTag":"","extMngdBy":"","floodOnEncap":"disabled","isSharedSrvMsiteEPg":"no","lcOwn":"local","matchT":"AtleastOne","mcast":"no","modTs":"2025-08-11T18:14:54.961+00:00","monPolDn":"uni/tn-common/monepg-default","name":"testExtEPG","nameAlias":"","pcEnfPref":"unenforced","pcTag":"10953","pcTagAllocSrc":"idmanager","prefGrMemb":"include","prio":"unspecified","scope":"2490368","status":"","targetDscp":"unspecified","triggerSt":"triggerable","txId":"1729382256913953508","uid":"15374","userdom":":all:"}, +"children":[{"fvRsProv":{"attributes":{"accessPrivilege":"USER","annotation":"","childAction":"","ctrctUpd":"ctrct","extMngdBy":"","forceResolve":"yes","intent":"install","lcOwn":"local","matchT":"AtleastOne","modTs":"2025-08-11T18:14:54.961+00:00","monPolDn":"uni/tn-common/monepg-default","prio":"unspecified","rType":"mo","rn":"rsprov-AD_C","state":"formed","stateQual":"none","status":"","tCl":"vzBrCP","tContextDn":"","tDn":"uni/tn-common/brc-AD_C","tRn":"brc-AD_C","tType":"name","tnVzBrCPName":"AD_C","triggerSt":"triggerable","uid":"15374","updateCollection":"no","userdom":":all:"}}}]}}] \ No newline at end of file diff --git a/tests/checks/pg_and_shared_svc_contract_check/global_vzBrCP_pos.json b/tests/checks/pg_and_shared_svc_contract_check/global_vzBrCP_pos.json new file mode 100644 index 0000000..ea548bf --- /dev/null +++ b/tests/checks/pg_and_shared_svc_contract_check/global_vzBrCP_pos.json @@ -0,0 +1,24 @@ +[ + {"vzBrCP":{ + "attributes":{ + "accessPrivilege":"USER", + "annotation":"", + "childAction":"", + "configIssues":"", + "descr":"", + "dn":"uni/tn-common/brc-AD_C", + "name":"AD_C", + "scope":"global" + } + } + }, + {"vzBrCP":{ + "attributes":{ + "accessPrivilege":"USER", + "dn":"uni/tn-laumende/brc-test", + "name":"test", + "scope":"global" + } + } + } + ] \ No newline at end of file diff --git a/tests/checks/pg_and_shared_svc_contract_check/test_pg_and_shared_svc_contract_check.py b/tests/checks/pg_and_shared_svc_contract_check/test_pg_and_shared_svc_contract_check.py new file mode 100644 index 0000000..ae93129 --- /dev/null +++ b/tests/checks/pg_and_shared_svc_contract_check/test_pg_and_shared_svc_contract_check.py @@ -0,0 +1,781 @@ +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 = "pg_and_shared_svc_contract_check" + +# icurl queries +# shared contracts +shrd_contracts_api = 'vzBrCP.json' +shrd_contracts_api += '?query-target-filter=or(eq(vzBrCP.scope,"global"),eq(vzBrCP.scope,"tenant"))' + +# global epgs (17 <= pcTag <= 16385) with Preferred Group enabled and provided contracts + +glbl_epgs_api = 'fvAEPg.json' +glbl_epgs_api += '?query-target-filter=and(le(fvAEPg.pcTag,"16385"),ge(fvAEPg.pcTag,"17"),eq(fvAEPg.prefGrMemb,"include"))' +glbl_epgs_api += '&rsp-subtree=children&rsp-subtree-class=fvRsProv' + +# global external EPGs (17 <= pcTag <= 16385) with Preferred Group enabled and provided contracts + +glbl_ext_epgs_api = 'l3extInstP.json' +glbl_ext_epgs_api += '?query-target-filter=and(le(l3extInstP.pcTag,"16385"),ge(l3extInstP.pcTag,"17"),eq(l3extInstP.prefGrMemb,"include"))' +glbl_ext_epgs_api += '&rsp-subtree=children&rsp-subtree-class=fvRsProv' + +ctx_defs_api = 'fvCtxDef.json' +provider_relationships_api = 'vzFromEPg.json' +provider_relationships_api += '?query-target-filter=and(eq(vzFromEPg.membType,"prov"),' +provider_relationships_api += 'le(vzFromEPg.pcTag,"16385"),ge(vzFromEPg.pcTag,"17"))' +provider_relationships_api += '&rsp-subtree=children&rsp-subtree-class=vzToEPg' + +childless_fvAEPg = { + "fvAEPg": { + "attributes": { + "dn": "uni/tn-test/ap-test/epg-no-provider", + "pcTag": "100" + } + } +} +childless_l3extInstP = { + "l3extInstP": { + "attributes": { + "dn": "uni/tn-test/out-test/instP-no-provider", + "pcTag": "101" + } + } +} +provider_dn = "uni/tn-common/ap-apptest/epg-epg1" +provider_scope = "2261001" +provider_ctx_def_dn = "uni/ctx-[uni/tn-common/ctx-provider]" +external_provider_dn = "uni/tn-common/out-test-L3Out/instP-testExtEPG" +external_provider_scope = "2490368" +external_provider_ctx_def_dn = "uni/ctx-[uni/tn-common/ctx-external-provider]" +ordinary_consumer_dn = "uni/tn-consumer/ap-app/epg-consumer" +different_vrf_l3out_consumer_dn = "uni/tn-consumer/out-consumer/instP-different-vrf" +same_vrf_l3out_consumer_dn = "uni/tn-consumer/out-consumer/instP-same-vrf" +different_ctx_def_dn = "uni/ctx-[uni/tn-consumer/ctx-consumer]" + + +def ctx_def(scope, ctx_def_dn): + return { + "fvCtxDef": { + "attributes": { + "dn": ctx_def_dn, + "scope": scope + } + } + } + + +def provider_relationship( + contract, + provider, + provider_scope_id, + consumer, + consumer_ctx_def_dn, + consumer_scope_id="999" +): + return { + "vzFromEPg": { + "attributes": { + "dn": "cdef-[{}]/epgCont-[{}]/fr-[provider]".format( + contract, + provider + ), + "epgDn": provider, + "membType": "prov", + "scopeId": provider_scope_id + }, + "children": [ + { + "vzToEPg": { + "attributes": { + "ctxDefDn": consumer_ctx_def_dn, + "dn": ( + "cdef-[{}]/epgCont-[{}]/fr-[provider]/" + "to-[{}]" + ).format(contract, provider, consumer), + "epgDn": consumer, + "scopeId": consumer_scope_id + } + } + } + ] + } + } + + +provider_ctx_defs = [ + ctx_def(provider_scope, provider_ctx_def_dn), + ctx_def(external_provider_scope, external_provider_ctx_def_dn) +] +cross_context_ordinary_relationship = provider_relationship( + "uni/tn-common/brc-AD_C", + provider_dn, + provider_scope, + ordinary_consumer_dn, + different_ctx_def_dn +) +same_context_ordinary_relationship = provider_relationship( + "uni/tn-common/brc-AD_C", + provider_dn, + provider_scope, + ordinary_consumer_dn, + provider_ctx_def_dn, + provider_scope +) +cross_context_l3out_relationship = provider_relationship( + "uni/tn-common/brc-AD_C", + provider_dn, + provider_scope, + different_vrf_l3out_consumer_dn, + different_ctx_def_dn +) +same_context_l3out_relationship = provider_relationship( + "uni/tn-common/brc-AD_C", + provider_dn, + provider_scope, + same_vrf_l3out_consumer_dn, + provider_ctx_def_dn, + provider_scope +) +external_provider_l3out_relationship = provider_relationship( + "uni/tn-common/brc-AD_C", + external_provider_dn, + external_provider_scope, + different_vrf_l3out_consumer_dn, + different_ctx_def_dn +) +tenant_contract = { + "vzBrCP": { + "attributes": { + "dn": "uni/tn-test/brc-tenant-shared", + "name": "tenant-shared", + "scope": "tenant" + } + } +} +tenant_provider = { + "fvAEPg": { + "attributes": { + "dn": "uni/tn-test/ap-provider/epg-provider", + "pcTag": "102", + "scope": "1000" + }, + "children": [ + { + "fvRsProv": { + "attributes": { + "tDn": "uni/tn-test/brc-tenant-shared" + } + } + } + ] + } +} +tenant_provider_ctx_def_dn = "uni/ctx-[uni/tn-test/ctx-provider]" +tenant_consumer_ctx_def_dn = "uni/ctx-[uni/tn-test/ctx-consumer]" +tenant_l3out_consumer_dn = "uni/tn-test/out-consumer/instP-consumer" +tenant_vzany_consumer_dn = "uni/tn-test/ctx-consumer/any" +tenant_mismatch_consumer_dn = "uni/tn-other/out-consumer/instP-consumer" +tenant_ctx_defs = [ + ctx_def("1000", tenant_provider_ctx_def_dn) +] +tenant_l3out_relationship = provider_relationship( + "uni/tn-test/brc-tenant-shared", + "uni/tn-test/ap-provider/epg-provider", + "1000", + tenant_l3out_consumer_dn, + tenant_consumer_ctx_def_dn, + "2000" +) +tenant_vzany_relationship = provider_relationship( + "uni/tn-test/brc-tenant-shared", + "uni/tn-test/ap-provider/epg-provider", + "1000", + tenant_vzany_consumer_dn, + tenant_consumer_ctx_def_dn, + "2000" +) +tenant_mismatch_relationship = provider_relationship( + "uni/tn-test/brc-tenant-shared", + "uni/tn-test/ap-provider/epg-provider", + "1000", + tenant_mismatch_consumer_dn, + "uni/ctx-[uni/tn-other/ctx-consumer]", + "3000" +) + + +@pytest.mark.parametrize( + "icurl_outputs, cversion, tversion, expected_result", + [ + + # MANUAL cases + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: read_data(dir, "global_pg_fvAEPg.json"), + glbl_ext_epgs_api: read_data(dir, "global_pg_l3extInstP.json"), + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [cross_context_ordinary_relationship] + }, + "4.2(4a)", None, + script.MANUAL, + ), + # NA cases + # Target version is lower than 4.2, Result = NA + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: read_data(dir, "global_pg_fvAEPg.json"), + glbl_ext_epgs_api: read_data(dir, "global_pg_l3extInstP.json"), + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [cross_context_ordinary_relationship] + }, + "4.2(1a)", "4.1(2a)", + script.NA, + ), + # Target version predates Preferred Group-specific F0467 enforcement, + # but the forwarding risk is still present. + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: read_data(dir, "global_pg_fvAEPg.json"), + glbl_ext_epgs_api: read_data(dir, "global_pg_l3extInstP.json"), + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [cross_context_ordinary_relationship] + }, + "4.2(1a)", "5.1(1g)", + script.FAIL_O, + ), + # There are no global contracts, Result = NA + ( + { + shrd_contracts_api: [], + glbl_epgs_api: read_data(dir, "global_pg_fvAEPg.json"), + glbl_ext_epgs_api: read_data(dir, "global_pg_l3extInstP.json") + }, + "4.2(1a)", "6.1(1g)", + script.NA, + ), + # FAIL_O Cases + # Target version is older than 6.0(1g), Result = FAIL_O + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: read_data(dir, "global_pg_fvAEPg.json"), + glbl_ext_epgs_api: read_data(dir, "global_pg_l3extInstP.json"), + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [cross_context_ordinary_relationship] + }, + "4.2(1a)", "6.0(1f)", + script.FAIL_O, + ), + # Tenant-scope contracts carry the same broad forwarding risk through 5.2. + ( + { + shrd_contracts_api: [tenant_contract], + glbl_epgs_api: [tenant_provider], + glbl_ext_epgs_api: [], + ctx_defs_api: tenant_ctx_defs, + provider_relationships_api: [tenant_l3out_relationship] + }, + "4.2(1a)", "5.2(8i)", + script.FAIL_O, + ), + # Target version is newer than 6.0(1g), both global_pg EPGs and extEPGs , Result = FAIL_O + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: read_data(dir, "global_pg_fvAEPg.json"), + glbl_ext_epgs_api: read_data(dir, "global_pg_l3extInstP.json"), + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [cross_context_l3out_relationship] + }, + "4.2(1a)", "6.0(1g)", + script.FAIL_O, + ), + # Target version is newer than 6.0(1g), no EPGS, only global_pg extEPGs , Result = FAIL_O + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: [], + glbl_ext_epgs_api: read_data(dir, "global_pg_l3extInstP.json"), + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [external_provider_l3out_relationship] + }, + "4.2(1a)", "6.0(1g)", + script.FAIL_O, + ), + # A childless fvAEPg does not hide a later affected fvAEPg. + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: [childless_fvAEPg] + read_data(dir, "global_pg_fvAEPg.json"), + glbl_ext_epgs_api: [], + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [cross_context_ordinary_relationship] + }, + "4.2(1a)", "5.2(8i)", + script.FAIL_O, + ), + # A childless l3extInstP does not hide a later affected l3extInstP. + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: [], + glbl_ext_epgs_api: [childless_l3extInstP] + read_data(dir, "global_pg_l3extInstP.json"), + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [external_provider_l3out_relationship] + }, + "4.2(1a)", "6.0(1g)", + script.FAIL_O, + ), + # PASS Cases + # Target version is older than 6.0(1g), no global_pg EPGs or extEPGs , Result = PASS + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: [], + glbl_ext_epgs_api: [] + }, + "4.2(1a)", "6.0(1f)", + script.PASS, + ), + # Target version is newer than 6.0(1g), no global_pg EPGs or extEPGs , Result = PASS + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: [], + glbl_ext_epgs_api: [] + }, + "4.2(1a)", "6.0(1h)", + script.PASS, + ), + # Target version is newer than 6.0(1g), only global_pg EPGs , no global_pg extEPGs , Result = PASS + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: read_data(dir, "global_pg_fvAEPg.json"), + glbl_ext_epgs_api: [], + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [] + }, + "4.2(1a)", "6.0(1h)", + script.PASS, + ), + # Preferred-group objects without provider children are not affected. + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: [childless_fvAEPg], + glbl_ext_epgs_api: [childless_l3extInstP] + }, + "4.2(1a)", "5.2(8i)", + script.PASS, + ), + # No L3Out consumer means an ordinary EPG consumer cannot trigger this check. + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: read_data(dir, "global_pg_fvAEPg.json"), + glbl_ext_epgs_api: [], + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [cross_context_ordinary_relationship] + }, + "4.2(1a)", "6.0(1g)", + script.PASS, + ), + # A same-VRF L3Out consumer does not trigger the cross-VRF restriction. + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: [read_data(dir, "global_pg_fvAEPg.json")[0]], + glbl_ext_epgs_api: [], + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [same_context_l3out_relationship] + }, + "4.2(1a)", "6.0(1g)", + script.PASS, + ), + # Unrelated L3Out and vzAny consumers do not affect the provider. + ( + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: read_data(dir, "global_pg_fvAEPg.json"), + glbl_ext_epgs_api: [], + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [] + }, + "4.2(1a)", "6.0(1g)", + script.PASS, + ), + ] +) +def test_logic(run_check, mock_icurl, cversion, tversion, expected_result): + result = run_check( + cversion=script.AciVersion(cversion), + tversion=script.AciVersion(tversion) if tversion else None + ) + assert result.result == expected_result + + +def test_provider_queries_exclude_reserved_and_local_pctags(run_check, monkeypatch): + queries = [] + + def recording_icurl(apitype, query, page=0, page_size=100000): + queries.append(query) + if query == shrd_contracts_api: + return [tenant_contract] + return [] + + monkeypatch.setattr(script, "icurl", recording_icurl) + + result = run_check( + cversion=script.AciVersion("5.2(8i)"), + tversion=script.AciVersion("5.2(8i)") + ) + + assert result.result == script.PASS + assert queries == [shrd_contracts_api, glbl_epgs_api, glbl_ext_epgs_api] + assert 'ge(fvAEPg.pcTag,"17")' in glbl_epgs_api + assert 'le(fvAEPg.pcTag,"16385")' in glbl_epgs_api + assert 'ge(l3extInstP.pcTag,"17")' in glbl_ext_epgs_api + assert 'le(l3extInstP.pcTag,"16385")' in glbl_ext_epgs_api + assert 'ge(vzFromEPg.pcTag,"17")' in provider_relationships_api + assert 'le(vzFromEPg.pcTag,"16385")' in provider_relationships_api + + +@pytest.mark.parametrize( + "icurl_outputs", + [ + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: read_data(dir, "global_pg_fvAEPg.json"), + glbl_ext_epgs_api: read_data(dir, "global_pg_l3extInstP.json"), + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [cross_context_l3out_relationship] + } + ] +) +@pytest.mark.parametrize( + "tversion", + [ + "4.2(5n)", + "4.2(6d)", + "5.1(1h)", + "5.1(3e)", + "5.2(1g)", + "5.2(8i)", + "6.0(1g)", + ] +) +def test_all_4_2_and_newer_targets_are_checked(run_check, mock_icurl, tversion): + result = run_check( + cversion=script.AciVersion("4.2(1a)"), + tversion=script.AciVersion(tversion) + ) + assert result.result not in (script.NA, script.ERROR) + + +@pytest.mark.parametrize( + "icurl_outputs", + [ + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: [read_data(dir, "global_pg_fvAEPg.json")[0]], + glbl_ext_epgs_api: [], + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [cross_context_l3out_relationship] + } + ] +) +def test_reports_correlated_l3out_consumer(run_check, mock_icurl): + result = run_check( + cversion=script.AciVersion("5.2(8i)"), + tversion=script.AciVersion("6.0(1g)") + ) + + assert result.result == script.FAIL_O + assert result.data == [[ + "uni/tn-common/brc-AD_C", + "uni/tn-common/ap-apptest/epg-epg1", + "5555", + "uni/tn-consumer/out-consumer/instP-different-vrf" + ]] + assert "remove each listed provider from the Preferred Group" in result.recommended_action + assert "stop it from providing the listed shared-service contract" in result.recommended_action + assert "remove the unsupported L3Out/vzAny consumer relationship" in result.recommended_action + assert "F0467 or F4684" in result.recommended_action + assert result.doc_url.endswith("/#preferred-group-shared-service-provider") + + +@pytest.mark.parametrize( + "icurl_outputs", + [ + { + shrd_contracts_api: [tenant_contract], + glbl_epgs_api: [tenant_provider], + glbl_ext_epgs_api: [], + ctx_defs_api: tenant_ctx_defs, + provider_relationships_api: [tenant_l3out_relationship] + } + ] +) +def test_reports_tenant_scope_contract_across_vrfs(run_check, mock_icurl): + result = run_check( + cversion=script.AciVersion("5.2(8i)"), + tversion=script.AciVersion("6.0(1g)") + ) + + assert result.result == script.FAIL_O + assert result.data == [[ + "uni/tn-test/brc-tenant-shared", + "uni/tn-test/ap-provider/epg-provider", + "102", + "uni/tn-test/out-consumer/instP-consumer" + ]] + + +@pytest.mark.parametrize( + "icurl_outputs", + [ + { + shrd_contracts_api: [tenant_contract], + glbl_epgs_api: [tenant_provider], + glbl_ext_epgs_api: [], + ctx_defs_api: tenant_ctx_defs, + provider_relationships_api: [tenant_vzany_relationship] + } + ] +) +def test_reports_correlated_vzany_consumer(run_check, mock_icurl): + result = run_check( + cversion=script.AciVersion("5.2(8i)"), + tversion=script.AciVersion("6.0(1g)") + ) + + assert result.result == script.FAIL_O + assert result.data == [[ + "uni/tn-test/brc-tenant-shared", + "uni/tn-test/ap-provider/epg-provider", + "102", + "uni/tn-test/ctx-consumer/any" + ]] + + +@pytest.mark.parametrize( + "icurl_outputs", + [ + { + shrd_contracts_api: [tenant_contract], + glbl_epgs_api: [tenant_provider], + glbl_ext_epgs_api: [], + ctx_defs_api: tenant_ctx_defs, + provider_relationships_api: [provider_relationship( + "uni/tn-test/brc-tenant-shared", + "uni/tn-test/ap-provider/epg-provider", + "1000", + tenant_vzany_consumer_dn, + tenant_provider_ctx_def_dn, + "1000" + )] + } + ] +) +def test_same_context_vzany_consumer_is_not_reported(run_check, mock_icurl): + result = run_check( + cversion=script.AciVersion("5.2(8i)"), + tversion=script.AciVersion("6.0(1g)") + ) + + assert result.result == script.PASS + + +@pytest.mark.parametrize( + "icurl_outputs", + [ + { + shrd_contracts_api: [tenant_contract], + glbl_epgs_api: [tenant_provider], + glbl_ext_epgs_api: [], + ctx_defs_api: tenant_ctx_defs, + provider_relationships_api: [tenant_mismatch_relationship] + } + ] +) +def test_tenant_scope_relationship_requires_matching_tenant(run_check, mock_icurl): + result = run_check( + cversion=script.AciVersion("5.2(8i)"), + tversion=script.AciVersion("6.0(1g)") + ) + + assert result.result == script.PASS + + +@pytest.mark.parametrize( + "icurl_outputs", + [ + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: [read_data(dir, "global_pg_fvAEPg.json")[0]], + glbl_ext_epgs_api: [], + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [] + } + ] +) +def test_provider_without_materialized_relationship_is_not_reported( + run_check, + mock_icurl +): + result = run_check( + cversion=script.AciVersion("5.2(8i)"), + tversion=script.AciVersion("5.2(8i)") + ) + + assert result.result == script.PASS + + +@pytest.mark.parametrize( + "icurl_outputs", + [ + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: [read_data(dir, "global_pg_fvAEPg.json")[0]], + glbl_ext_epgs_api: [], + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [same_context_ordinary_relationship] + } + ] +) +def test_same_context_relationship_is_not_reported_before_6_0( + run_check, + mock_icurl +): + result = run_check( + cversion=script.AciVersion("5.2(8i)"), + tversion=script.AciVersion("5.2(8i)") + ) + + assert result.result == script.PASS + + +@pytest.mark.parametrize( + "icurl_outputs", + [ + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: [read_data(dir, "global_pg_fvAEPg.json")[0]], + glbl_ext_epgs_api: [], + ctx_defs_api: [], + provider_relationships_api: [cross_context_l3out_relationship] + } + ] +) +def test_missing_provider_context_is_an_error(run_check, mock_icurl): + result = run_check( + cversion=script.AciVersion("5.2(8i)"), + tversion=script.AciVersion("6.0(1g)") + ) + + assert result.result == script.ERROR + assert result.msg == ( + "Unable to resolve context for one or more derived contract relationships" + ) + assert result.data == [[ + ( + "cdef-[uni/tn-common/brc-AD_C]/" + "epgCont-[uni/tn-common/ap-apptest/epg-epg1]/fr-[provider]" + ), + "No fvCtxDef found for scopeId 2261001" + ]] + assert "Retry the check" in result.recommended_action + assert "contact Cisco TAC" in result.recommended_action + + +@pytest.mark.parametrize( + "icurl_outputs", + [ + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: [read_data(dir, "global_pg_fvAEPg.json")[0]], + glbl_ext_epgs_api: [], + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [provider_relationship( + "uni/tn-common/brc-AD_C", + provider_dn, + provider_scope, + different_vrf_l3out_consumer_dn, + "" + )] + } + ] +) +def test_missing_consumer_context_is_an_error(run_check, mock_icurl): + result = run_check( + cversion=script.AciVersion("5.2(8i)"), + tversion=script.AciVersion("6.0(1g)") + ) + + assert result.result == script.ERROR + assert result.msg == ( + "Unable to resolve context for one or more derived contract relationships" + ) + assert result.data == [[ + ( + "cdef-[uni/tn-common/brc-AD_C]/" + "epgCont-[uni/tn-common/ap-apptest/epg-epg1]/fr-[provider]/" + "to-[uni/tn-consumer/out-consumer/instP-different-vrf]" + ), + "vzToEPg.ctxDefDn is empty" + ]] + assert "Retry the check" in result.recommended_action + assert "contact Cisco TAC" in result.recommended_action + + +@pytest.mark.parametrize( + "icurl_outputs", + [ + { + shrd_contracts_api: read_data(dir, "global_vzBrCP_pos.json"), + glbl_epgs_api: [read_data(dir, "global_pg_fvAEPg.json")[0]], + glbl_ext_epgs_api: [], + ctx_defs_api: provider_ctx_defs, + provider_relationships_api: [ + cross_context_l3out_relationship, + provider_relationship( + "uni/tn-common/brc-AD_C", + provider_dn, + "missing-scope", + ordinary_consumer_dn, + different_ctx_def_dn + ) + ] + } + ] +) +def test_context_error_preserves_confirmed_affected_relationship( + run_check, + mock_icurl +): + result = run_check( + cversion=script.AciVersion("5.2(8i)"), + tversion=script.AciVersion("6.0(1g)") + ) + + assert result.result == script.ERROR + assert result.data[0][1] == "No fvCtxDef found for scopeId missing-scope" + assert result.unformatted_data == [[ + "uni/tn-common/brc-AD_C", + provider_dn, + "5555", + different_vrf_l3out_consumer_dn + ]]