From aa63be65d58942856990aab30b6640ea5e21c95a Mon Sep 17 00:00:00 2001 From: JASON YE Date: Wed, 17 Jun 2026 07:52:47 +0000 Subject: [PATCH] CSCwq69622 CX Script: Add Multi-site DPTEP / Routable TEP Subnet overlap check Mirror the APIC PD validation added by aci-apic/mgmt-as-is PR #11881 (CSCwq69622). The APIC rejects any new fvIntersiteConnP / fvIntersiteMcastConnP whose addr falls in the unreserved portion of any fabricExtRoutablePodSubnet pool. On older releases this configuration is allowed and may already be present, but after upgrading the offending MO can no longer be modified, and on a subsequent reconfiguration the upgrade fails to redistribute it. The check mirrors fabric::ExtRoutablePodSubnetBI::checkIfPartOfUnreservedSubnet exactly: for each IPv4 fabricExtRoutablePodSubnet, the unreserved range is [networkStart + reserveAddressCount, networkEnd]. Any fvIntersiteConnP / fvIntersiteMcastConnP whose addr falls in that range on any subnet is reported. IPv6 routable subnets are skipped because the corresponding APIC validation is IPv4-only. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- aci-preupgrade-validation-script.py | 71 ++++++++++++++++++ docs/docs/validations.md | 13 ++++ .../mcast_dptep_in_unreserved.json | 10 +++ .../no_dptep.json | 1 + .../no_routable_subnet.json | 1 + .../routable_subnet_only.json | 11 +++ ...ite_dptep_routable_subnet_overlap_check.py | 74 +++++++++++++++++++ .../ucast_dptep_in_reserved.json | 10 +++ .../ucast_dptep_in_unreserved.json | 10 +++ .../ucast_dptep_outside_pool.json | 10 +++ 10 files changed, 211 insertions(+) create mode 100644 tests/checks/multisite_dptep_routable_subnet_overlap_check/mcast_dptep_in_unreserved.json create mode 100644 tests/checks/multisite_dptep_routable_subnet_overlap_check/no_dptep.json create mode 100644 tests/checks/multisite_dptep_routable_subnet_overlap_check/no_routable_subnet.json create mode 100644 tests/checks/multisite_dptep_routable_subnet_overlap_check/routable_subnet_only.json create mode 100644 tests/checks/multisite_dptep_routable_subnet_overlap_check/test_multisite_dptep_routable_subnet_overlap_check.py create mode 100644 tests/checks/multisite_dptep_routable_subnet_overlap_check/ucast_dptep_in_reserved.json create mode 100644 tests/checks/multisite_dptep_routable_subnet_overlap_check/ucast_dptep_in_unreserved.json create mode 100644 tests/checks/multisite_dptep_routable_subnet_overlap_check/ucast_dptep_outside_pool.json diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index ae196700..8edb20d4 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -3770,6 +3770,76 @@ def isis_redis_metric_mpod_msite_check(**kwargs): return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) +@check_wrapper(check_title="Multi-site DPTEP overlap with Routable TEP Subnet") +def multisite_dptep_routable_subnet_overlap_check(**kwargs): + result = PASS + headers = ["DPTEP Type", "DPTEP Address", "Routable TEP Pool", "DPTEP DN"] + data = [] + recommended_action = ( + 'Change the overlapping Multi-site DPTEP address to fall outside the ' + 'unreserved portion of every Routable TEP Pool, or extend the Routable ' + 'TEP Pool reserved address count, before upgrading.' + ) + doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#multi-site-dptep-overlap-with-routable-tep-subnet' + + subnets = icurl('class', 'fabricExtRoutablePodSubnet.json') + if not subnets: + return Result(result=PASS, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) + + # Mirrors fabric::ExtRoutablePodSubnetBI::checkIfPartOfUnreservedSubnet: + # the unreserved range is [poolStart + reserveAddressCount, poolEnd]. + # IPv4-only, matching the PD validation. + subnet_ranges = [] + for s in subnets: + attrs = s['fabricExtRoutablePodSubnet']['attributes'] + pool = attrs.get('pool', '') + if '/' not in pool or ':' in pool: + continue + try: + ip_str, pfx_str = pool.split('/') + pfx = int(pfx_str) + start_ip = int(IPAddress.ipv4_to_binary(ip_str), 2) + mask = (0xFFFFFFFF << (32 - pfx)) & 0xFFFFFFFF if pfx else 0 + net_start = start_ip & mask + net_end = net_start | ((~mask) & 0xFFFFFFFF) + reserve = int(attrs.get('reserveAddressCount', '0') or '0') + except (ValueError, TypeError): + continue + unr_lo = net_start + reserve + unr_hi = net_end + if unr_lo > unr_hi: + continue + subnet_ranges.append((pool, unr_lo, unr_hi)) + + if not subnet_ranges: + return Result(result=PASS, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) + + def _check(conn_class, label): + mos = icurl('class', '{}.json'.format(conn_class)) + for mo in mos: + attrs = mo[conn_class]['attributes'] + addr_str = attrs.get('addr', '') + ip_only = addr_str.split('/')[0] + if not ip_only or ':' in ip_only: + continue + try: + ip_int = int(IPAddress.ipv4_to_binary(ip_only), 2) + except (ValueError, TypeError): + continue + for pool, lo, hi in subnet_ranges: + if lo <= ip_int <= hi: + data.append([label, addr_str, pool, attrs.get('dn', '')]) + break + + _check('fvIntersiteConnP', 'Unicast') + _check('fvIntersiteMcastConnP', 'Multicast') + + 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="BGP route target type for GOLF over L2EVPN") def bgp_golf_route_target_type_check(cversion, tversion, **kwargs): result = FAIL_O @@ -6584,6 +6654,7 @@ class CheckManager: l3out_overlapping_loopback_check, intersight_upgrade_status_check, isis_redis_metric_mpod_msite_check, + multisite_dptep_routable_subnet_overlap_check, bgp_golf_route_target_type_check, docker0_subnet_overlap_check, uplink_limit_check, diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 807c5abd..4b1b4562 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -139,6 +139,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: +[Multi-site DPTEP overlap with Routable TEP Subnet][c25] | :white_check_mark: | :white_check_mark: 6.2(2) [c1]: #vpc-paired-leaf-switches @@ -165,6 +166,7 @@ Items | Faults | This Script [c22]: #service-graph-bd-forceful-routing [c23]: #ave-end-of-life [c24]: #shared-service-with-vzany-consumer +[c25]: #multi-site-dptep-overlap-with-routable-tep-subnet ### Defect Condition Checks @@ -2090,6 +2092,17 @@ This script checks the ISIS Redistribution Metric via `redistribMetric` of an ob ``` +### Multi-site DPTEP overlap with Routable TEP Subnet + +Multi-site Data-Plane TEPs (DPTEPs) are configured under `Tenant infra > Policies > Protocol > Fabric Ext Connection Policies` and stored as `fvIntersiteConnP` (unicast DPTEP) and `fvIntersiteMcastConnP` (multicast DPTEP). The Routable TEP Pool, configured under `Fabric > Inventory > Pod Fabric Setup Policy > Pod > Routable TEP Pool`, is stored as `fabricExtRoutablePodSubnet` and is partitioned into a reserved range (used for fabric-allocated TEPs) followed by the remaining unreserved range. The number of reserved addresses is `reserveAddressCount`. + +Starting with APIC 6.2(2) (CSCwq69622), the APIC rejects any new `fvIntersiteConnP` / `fvIntersiteMcastConnP` whose `addr` falls in the unreserved portion of any Routable TEP Pool, because such DPTEPs may collide with addresses the fabric will allocate from that subnet. On older releases this configuration is allowed and may already be present in the running configuration, but after upgrading the offending MO can no longer be modified, and on a subsequent reconfiguration the upgrade will fail to redistribute it. + +This script flags every `fvIntersiteConnP` and `fvIntersiteMcastConnP` whose `addr` overlaps with the unreserved range of any `fabricExtRoutablePodSubnet`. The unreserved range used for the comparison is `[networkStart + reserveAddressCount, networkEnd]`, matching `fabric::ExtRoutablePodSubnetBI::checkIfPartOfUnreservedSubnet` in the APIC. IPv4 routable subnets are checked; IPv6 is skipped because the corresponding APIC validation is IPv4-only. + +Resolve any flagged overlap by changing the offending Multi-site DPTEP address to fall outside the unreserved portion of every Routable TEP Pool, or by extending the Routable TEP Pool reserved address count, before upgrading. + + ### BGP Route-target Type for GOLF over L2EVPN Prior to upgrading to release 4.2 or later, if you are using the ACI GOLF feature with **Explicit Route Targets**, you must ensure that all **Explicit Route Targets** point to a route-target policy explicitly configured with a `route-target` community type instead of `extended` (CSCvm23100). diff --git a/tests/checks/multisite_dptep_routable_subnet_overlap_check/mcast_dptep_in_unreserved.json b/tests/checks/multisite_dptep_routable_subnet_overlap_check/mcast_dptep_in_unreserved.json new file mode 100644 index 00000000..ea577e44 --- /dev/null +++ b/tests/checks/multisite_dptep_routable_subnet_overlap_check/mcast_dptep_in_unreserved.json @@ -0,0 +1,10 @@ +[ + { + "fvIntersiteMcastConnP": { + "attributes": { + "dn": "uni/tn-infra/fabricExtConnP-1/podConnP-1/intersiteMcastConnP-[10.0.0.100]", + "addr": "10.0.0.100" + } + } + } +] diff --git a/tests/checks/multisite_dptep_routable_subnet_overlap_check/no_dptep.json b/tests/checks/multisite_dptep_routable_subnet_overlap_check/no_dptep.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/tests/checks/multisite_dptep_routable_subnet_overlap_check/no_dptep.json @@ -0,0 +1 @@ +[] diff --git a/tests/checks/multisite_dptep_routable_subnet_overlap_check/no_routable_subnet.json b/tests/checks/multisite_dptep_routable_subnet_overlap_check/no_routable_subnet.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/tests/checks/multisite_dptep_routable_subnet_overlap_check/no_routable_subnet.json @@ -0,0 +1 @@ +[] diff --git a/tests/checks/multisite_dptep_routable_subnet_overlap_check/routable_subnet_only.json b/tests/checks/multisite_dptep_routable_subnet_overlap_check/routable_subnet_only.json new file mode 100644 index 00000000..bbed9e06 --- /dev/null +++ b/tests/checks/multisite_dptep_routable_subnet_overlap_check/routable_subnet_only.json @@ -0,0 +1,11 @@ +[ + { + "fabricExtRoutablePodSubnet": { + "attributes": { + "dn": "uni/controller/setuppol/setupp-1/extrtpodsubnet-[10.0.0.0/24]", + "pool": "10.0.0.0/24", + "reserveAddressCount": "16" + } + } + } +] diff --git a/tests/checks/multisite_dptep_routable_subnet_overlap_check/test_multisite_dptep_routable_subnet_overlap_check.py b/tests/checks/multisite_dptep_routable_subnet_overlap_check/test_multisite_dptep_routable_subnet_overlap_check.py new file mode 100644 index 00000000..1c25469d --- /dev/null +++ b/tests/checks/multisite_dptep_routable_subnet_overlap_check/test_multisite_dptep_routable_subnet_overlap_check.py @@ -0,0 +1,74 @@ +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 = "multisite_dptep_routable_subnet_overlap_check" + +# icurl queries +subnet_api = 'fabricExtRoutablePodSubnet.json' +ucast_api = 'fvIntersiteConnP.json' +mcast_api = 'fvIntersiteMcastConnP.json' + + +@pytest.mark.parametrize( + "icurl_outputs, expected_result", + [ + # No Routable TEP Pool configured -> PASS without querying DPTEPs + ({subnet_api: read_data(dir, "no_routable_subnet.json")}, script.PASS), + # Routable TEP Pool present, no DPTEPs -> PASS + ( + { + subnet_api: read_data(dir, "routable_subnet_only.json"), + ucast_api: read_data(dir, "no_dptep.json"), + mcast_api: read_data(dir, "no_dptep.json"), + }, + script.PASS, + ), + # Unicast DPTEP outside of every Routable TEP Pool -> PASS + ( + { + subnet_api: read_data(dir, "routable_subnet_only.json"), + ucast_api: read_data(dir, "ucast_dptep_outside_pool.json"), + mcast_api: read_data(dir, "no_dptep.json"), + }, + script.PASS, + ), + # Unicast DPTEP inside the reserved portion of a Routable TEP Pool -> PASS + ( + { + subnet_api: read_data(dir, "routable_subnet_only.json"), + ucast_api: read_data(dir, "ucast_dptep_in_reserved.json"), + mcast_api: read_data(dir, "no_dptep.json"), + }, + script.PASS, + ), + # Unicast DPTEP overlapping the unreserved portion of a Routable TEP Pool -> FAIL + ( + { + subnet_api: read_data(dir, "routable_subnet_only.json"), + ucast_api: read_data(dir, "ucast_dptep_in_unreserved.json"), + mcast_api: read_data(dir, "no_dptep.json"), + }, + script.FAIL_O, + ), + # Multicast DPTEP overlapping the unreserved portion of a Routable TEP Pool -> FAIL + ( + { + subnet_api: read_data(dir, "routable_subnet_only.json"), + ucast_api: read_data(dir, "no_dptep.json"), + mcast_api: read_data(dir, "mcast_dptep_in_unreserved.json"), + }, + script.FAIL_O, + ), + ], +) +def test_logic(run_check, mock_icurl, expected_result): + result = run_check() + assert result.result == expected_result diff --git a/tests/checks/multisite_dptep_routable_subnet_overlap_check/ucast_dptep_in_reserved.json b/tests/checks/multisite_dptep_routable_subnet_overlap_check/ucast_dptep_in_reserved.json new file mode 100644 index 00000000..748a4a33 --- /dev/null +++ b/tests/checks/multisite_dptep_routable_subnet_overlap_check/ucast_dptep_in_reserved.json @@ -0,0 +1,10 @@ +[ + { + "fvIntersiteConnP": { + "attributes": { + "dn": "uni/tn-infra/fabricExtConnP-1/podConnP-1/intersiteConnP-[10.0.0.5]", + "addr": "10.0.0.5" + } + } + } +] diff --git a/tests/checks/multisite_dptep_routable_subnet_overlap_check/ucast_dptep_in_unreserved.json b/tests/checks/multisite_dptep_routable_subnet_overlap_check/ucast_dptep_in_unreserved.json new file mode 100644 index 00000000..721406b2 --- /dev/null +++ b/tests/checks/multisite_dptep_routable_subnet_overlap_check/ucast_dptep_in_unreserved.json @@ -0,0 +1,10 @@ +[ + { + "fvIntersiteConnP": { + "attributes": { + "dn": "uni/tn-infra/fabricExtConnP-1/podConnP-1/intersiteConnP-[10.0.0.50]", + "addr": "10.0.0.50" + } + } + } +] diff --git a/tests/checks/multisite_dptep_routable_subnet_overlap_check/ucast_dptep_outside_pool.json b/tests/checks/multisite_dptep_routable_subnet_overlap_check/ucast_dptep_outside_pool.json new file mode 100644 index 00000000..cc87b602 --- /dev/null +++ b/tests/checks/multisite_dptep_routable_subnet_overlap_check/ucast_dptep_outside_pool.json @@ -0,0 +1,10 @@ +[ + { + "fvIntersiteConnP": { + "attributes": { + "dn": "uni/tn-infra/fabricExtConnP-1/podConnP-1/intersiteConnP-[10.0.1.5]", + "addr": "10.0.1.5" + } + } + } +]