Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions aci-preupgrade-validation-script.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions docs/docs/validations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"fvIntersiteMcastConnP": {
"attributes": {
"dn": "uni/tn-infra/fabricExtConnP-1/podConnP-1/intersiteMcastConnP-[10.0.0.100]",
"addr": "10.0.0.100"
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
]
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"fvIntersiteConnP": {
"attributes": {
"dn": "uni/tn-infra/fabricExtConnP-1/podConnP-1/intersiteConnP-[10.0.0.5]",
"addr": "10.0.0.5"
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"fvIntersiteConnP": {
"attributes": {
"dn": "uni/tn-infra/fabricExtConnP-1/podConnP-1/intersiteConnP-[10.0.0.50]",
"addr": "10.0.0.50"
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"fvIntersiteConnP": {
"attributes": {
"dn": "uni/tn-infra/fabricExtConnP-1/podConnP-1/intersiteConnP-[10.0.1.5]",
"addr": "10.0.1.5"
}
}
}
]