Skip to content
84 changes: 83 additions & 1 deletion aci-preupgrade-validation-script.py
Original file line number Diff line number Diff line change
Expand Up @@ -6797,6 +6797,88 @@ def infravlan_overlap_access_policy_check(tversion, **kwargs):
return Result(result=result, msg=msg, headers=headers, data=data, unformatted_headers=unformatted_headers, unformatted_data=unformatted_data, recommended_action=recommended_action, doc_url=doc_url)


@check_wrapper(check_title="APIC OOB Connectivity check")
def apic_oob_connectivity_check(cversion, tversion, **kwargs):
result = PASS
headers = ["Node ID", "OOB IP", "Port", "Status"]
recommended_action = "Restore OOB management connectivity between all APICs and ensure the required HTTPS ports are reachable across the OOB network."
doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#apic-oob-connectivity'

def get_apic_oob_connectivity(apic_id_ip, port):
data = []
has_error = False

for apic in apic_id_ip:
attrs = apic['topSystem']['attributes']

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] Parse controller objects independently so malformed inventory does not erase valid evidence.

Direct indexing here lets one malformed object escape to the wrapper, which replaces the whole result with a generic ERROR and discards any unreachable controllers already collected. Validate each object's shape, preserve valid failure rows, and report malformed entries through unformatted_data or an error indication. Add a mixed valid-plus-malformed fixture.

node_id = attrs.get('id', '')

if attrs.get('oobMgmtAddr', '0.0.0.0') != '0.0.0.0':
ip = attrs.get('oobMgmtAddr')
elif attrs.get('oobMgmtAddr6', '::') not in ('', '::', '0:0:0:0:0:0:0:0'):
ip = attrs.get('oobMgmtAddr6')
else:
Comment on lines +6817 to +6819

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curl --max-time 5 -k -s https://[2001:db8:abc:1::12]:443 -- make sure you use proper format for ipv6. please check why pytest is noyt handling this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

continue

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocking] Do not skip a controller that has no usable OOB address.

Skipping it allows the new upgrade-blocking validation to return PASS without checking that peer. Return FAIL_UF with node-level evidence such as OOB address not configured. Also reject link-local-only IPv6 unless the required scope/interface is available: on the live 6.0(8e APIC, topSystem.oobMgmtAddr6 contained fe80:: addresses while mgmtRsOoBStNode.v6Addr was ::. Please cover no-address, link-local-only, IPv4-only, and configured global-IPv6 cases.


try:
ip_formatted = '[{}]'.format(ip) if ':' in ip else ip
with open(os.devnull, 'wb') as devnull:
if subprocess.call(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocking] Verify that this probe topology satisfies APIC-to-APIC reachability.

This runs every curl from only the APIC executing the script. That proves one source can reach each OOB address, not that every upgrade-fanout source can reach every peer. Please document and validate the product guarantee that the executing APIC is the sole relevant fanout source, or perform the supported remote checks needed to cover every required source-to-peer path. Without that guarantee, the check can pass while another APIC-to-APIC path is broken.

['curl', '--max-time', '5', '-k', '-s', '-o', os.devnull,
'https://{}:{}'.format(ip_formatted, port)],
stderr=devnull
) != 0:
data.append([node_id, ip, port, "Unreachable"])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low] Keep result-table payload values string-normalized.

Append str(port) rather than an integer and update the payload assertions. This keeps the new rows consistent across sorting, terminal formatting, and JSON consumers.

except Exception as e:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low] Consider avoiding shell=True in subprocess call

Using shell=True with string formatting is generally discouraged. While not a security risk here (IPs come from trusted APIC API), using a list is cleaner and more portable:

if subprocess.call(
    ['curl', '--max-time', '5', '-k', '-s', '-o', '/dev/null',
     'https://{}:{}'.format(ip_formatted, port)],
    stderr=subprocess.DEVNULL
) != 0:

This is a minor suggestion and not blocking.

log.error("Exception checking OOB connectivity for node %s: %s", node_id, e)
data.append([node_id, ip, port, "Error"])
has_error = True
continue

return data, has_error

if not tversion:
return Result(result=MANUAL, msg=TVER_MISSING)

if tversion.older_than("6.0(2a)"):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocking] Reconcile this applicability gate with the defect contract.

The check definition says tversion >= 6.0(2), while the CSCwu91693 RNE condition says the running APIC is 6.0.2 or later. Those differ when an upgrade crosses 6.0(2). Please obtain defect-owner confirmation, then align the implementation and documentation to one explicit current/target matrix. Add cases for current below 6.0(2) with target at/above it, and current at/above 6.0(2).

return Result(result=NA, msg=VER_NOT_AFFECTED)

apic_id_ip = icurl('class', 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")')
if not apic_id_ip:
return Result(result=NA, msg="No APIC controller nodes found.")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocking] Empty or incomplete required inventory must fail closed.

N/A becomes a passed, hidden validation, but a running APIC cannot legitimately establish this check with zero controllers. Return ERROR for an empty response. Also compare the returned controller IDs with the controller inventory already available to the script so a partial topSystem response cannot produce PASS. Add empty and partial inventory tests.


data = []
has_error = False

# Default port check: APIC bootx uses port 443 from 6.0(2)
default_data, default_error = get_apic_oob_connectivity(apic_id_ip, 443)
data.extend(default_data)
if default_error:
has_error = True

# Custom HTTPS port check: upgrade fanout uses commHttps port from 6.2(1)
if not cversion.older_than("6.2(1g)"):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocking] Enforce both custom-port version conditions.

The defined upgrade-fanout logic applies the custom HTTPS port only when both current and target releases are at or above the applicable 6.2 boundary. This branch checks only cversion, so a 6.2 current release targeting below the boundary still probes the custom port. Please add the target-version condition and a complete current/target boundary matrix using the defect-owner-confirmed CCO version.

port = 443
commHttps = icurl('class', 'commHttps.json')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocking] Resolve the effective commHttps policy deterministically and fail closed.

A class query may return default and non-default communication policies, so selecting [0] relies on undefined response order. Hard-coding comm-default is not correct either because a non-default policy may be active. Determine the policy actually used by upgrade fanout through the authoritative APIC configuration/relationship, then read that policy's commHttps child. Return ERROR when the effective policy or port is missing, malformed, or ambiguous. Add fixtures where default and non-default policies coexist and the non-default policy is effective. The live APIC had only comm-default, so this selection behavior still requires validation on an appropriate fabric.

if commHttps:
try:
port = int(commHttps[0]['commHttps']['attributes'].get('port', 443))
except (ValueError, KeyError):
log.warning("Could not read commHttps port")
return Result(result=ERROR, msg="Could not read https port id from commHttps MO.")

if port != 443: # Port 443 already covered by default port check above
custom_data, custom_error = get_apic_oob_connectivity(apic_id_ip, port)
data.extend(custom_data)
if custom_error:
has_error = True

if has_error:
result = ERROR
elif data:
result = FAIL_UF
return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url)


# ---- Script Execution ----


Expand Down Expand Up @@ -6973,7 +7055,7 @@ class CheckManager:
n9k_c93180yc_fx3_switch_memory_check,
stale_dbgacEpgSummaryTask_check,
infravlan_overlap_access_policy_check,

apic_oob_connectivity_check,
]
ssh_checks = [
# General
Expand Down
11 changes: 11 additions & 0 deletions docs/docs/validations.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ Items | Defect | This Script
[N9K-C93180YC-FX3 Switch Memory Less Than 32GB][d36] | CSCwm42741 | :white_check_mark: | :no_entry_sign:
[Stale dbgacEpgSummaryTask Objects][d37] | CSCwt69100 | :white_check_mark: | :no_entry_sign:
[InfraVLAN Overlap in Access Policy VLAN Pools][d38] | CSCwt58626 | :white_check_mark: | :no_entry_sign:
[APIC OOB Connectivity][d39] | CSCwu91693 | :white_check_mark: | :no_entry_sign:

[d1]: #ep-announce-compatibility
[d2]: #eventmgr-db-size-defect-susceptibility
Expand Down Expand Up @@ -246,6 +247,7 @@ Items | Defect | This Script
[d36]: #n9k-c93180yc-fx3-switch-memory-less-than-32gb
[d37]: #stale-dbgacepgsummarytask-objects
[d38]: #infravlan-overlap-access-policy-check
[d39]: #apic-oob-connectivity

## General Check Details

Expand Down Expand Up @@ -2858,6 +2860,14 @@ Due to the bug [CSCwt58626][77] , If Apic upgrade planned for target versions 6.

To avoid this issue, modify the user VLAN pool ranges so that the InfraVLAN does not overlap with any configured block, or select a non-impacted fixed version. After upgrading to a fixed version this fault and Restriction have been removed.

### APIC OOB Connectivity

Starting from 6.0(2), APIC firmware upgrades are triggered via an HTTPS POST request (bootx) sent to each peer APIC over its out-of-band (OOB) management interface. Due to [CSCwu91693][78], if OOB connectivity to a peer APIC is unavailable at the time this trigger is sent, that APIC does not receive it and silently fails to start the upgrade, while the remaining reachable APICs proceed normally. This results in a partially upgraded cluster with no explicit error raised at the time of failure.

This check verifies OOB reachability between APICs on the port(s) actually used for the upgrade trigger. The default port 443 is validated on all versions from 6.0(2) onward, since it is always used unless a custom HTTPS port is configured. From 6.2(1) onward, the upgrade trigger also honors a custom HTTPS port if one is configured via the `commHttps` policy; this custom port is validated only when the current version is 6.2(1) or later, and only when the configured port differs from 443, which is already covered by the default check.

For each applicable port, the script gets OOB management IP of every APIC in the cluster, then attempts an HTTPS connection to each peer on that port with a 5-second timeout. If any APIC is found unreachable on a port used for the upgrade trigger, the check fails.

[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
[2]: https://www.cisco.com/c/en/us/support/switches/nexus-9000-series-switches/products-release-notes-list.html
Expand Down Expand Up @@ -2935,3 +2945,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://bst.cloudapps.cisco.com/bugsearch/bug/CSCwu91693
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"commHttps": {
"attributes": {
"port": "8443"
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"commHttps": {
"attributes": {
"port": "443"
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"commHttps": {"attributes": {"port": "invalid"}}}]
Loading
Loading