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
43 changes: 43 additions & 0 deletions aci-preupgrade-validation-script.py
Original file line number Diff line number Diff line change
Expand Up @@ -6025,6 +6025,48 @@ 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="presListener MO Status")
def pres_listener_mo_check(fabric_nodes, tversion, **kwargs):
result = PASS
msg = ''
headers = ['Pod-ID', 'Node-ID', 'State']
data = []
recommended_action = 'Contact TAC to apply the workaround for the listed Nodes BEFORE Upgrade.'
doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations#missing-preslistener-mo'
# fabricNode.fabricSt shows `disabled` for both Decommissioned and Maintenance (GIR).
# fabricRsDecommissionNode.debug==yes is required to show `disabled (Maintenance)`.
presListener = icurl('class','presListener.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.

High: This full-class query runs before the target-version guards. Therefore a missing or unaffected target can become ERROR if collection fails even though the result should be MANUAL or N/A, and every run unnecessarily retrieves all presListener objects. Move the guards before collection and use a server-side filter for the relevant class-4307/node entries. Please test that icurl is not called for missing or unaffected targets.

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

if tversion.newer_than("6.1(3f)"):

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: The exact release boundary is not documented or tested: this condition keeps 6.1(3f) in the affected range, while the tests jump from 6.1(3a) to 6.1(4a). Reconcile the threshold with CSCwn81692, document the affected/fixed releases, and add tests immediately below, exactly at, and immediately above the fixed boundary.

result = NA
return Result(result=result, msg=msg, headers=headers, data=data, recommended_action=recommended_action)

for fabric_node in fabric_nodes:
found = False
if fabric_node['fabricNode']['attributes']['role'] != "leaf":
continue
state = fabric_node['fabricNode']['attributes']['fabricSt']
if state != 'active':
continue
dn = re.search(node_regex, fabric_node['fabricNode']['attributes']['dn'])

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: The regex result is dereferenced without validation. One malformed active-leaf DN raises an exception, changes the whole check to ERROR, and discards any missing-MO evidence already collected. Preserve valid findings and report malformed objects through the existing unformatted-data mechanism; add a mixed valid/malformed regression test.

pod_id = dn.group("pod")
node_id = dn.group("node")
for mo in presListener:
if (
node_id in mo['presListener']['attributes']['lstDn'] and

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: This substring check treats leaf 101 as present when the only MO belongs to 1101, producing a false PASS and potentially missing the documented port outage. Parse the terminal node ID or compare lstDn with the exact expected leaf path. Please add a 101/1101 collision test that expects FAIL_O and verifies the reported node.

'class-4307' in mo['presListener']['attributes']['dn']
):
found = True
continue
if not found:
result = FAIL_O
data.append([pod_id, node_id, state])
if not fabric_nodes:

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: fabric_nodes normally includes controllers and spines, so a non-empty inventory with no active leaves skips every entry but still returns PASS. Track how many active leaves were actually evaluated and return MANUAL when that count is zero. Please cover controller-only and disabled-leaf inventories.

result = MANUAL
msg = 'Active Switch Leaf fabricNode not found!'
return Result(result=result, msg=msg, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url)

# ---- Script Execution ----

Expand Down Expand Up @@ -6188,6 +6230,7 @@ class CheckManager:
standby_sup_sync_check,
isis_database_byte_check,
configpush_shard_check,
pres_listener_mo_check,

]
ssh_checks = [
Expand Down
12 changes: 12 additions & 0 deletions docs/docs/validations.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ Items | Defect | This Script
[Stale pconsRA Object][d26] | CSCwp22212 | :warning:{title="Deprecated"} | :no_entry_sign:
[ISIS DTEPs Byte Size][d27] | CSCwp15375 | :white_check_mark: | :no_entry_sign:
[Policydist configpushShardCont Crash][d28] | CSCwp95515 | :white_check_mark: |
[Missing presListener MO][d29] | CSCwn81692 | :white_check_mark: |

[d1]: #ep-announce-compatibility
[d2]: #eventmgr-db-size-defect-susceptibility
Expand Down Expand Up @@ -222,6 +223,7 @@ Items | Defect | This Script
[d26]: #stale-pconsra-object
[d27]: #isis-dteps-byte-size
[d28]: #policydist-configpushshardcont-crash
[d29]: #missing-preslistener-mo


## General Check Details
Expand Down Expand Up @@ -2648,6 +2650,15 @@ Due to [CSCwp95515][59], upgrading to an affected version while having any `conf
If any instances of `configpushShardCont` are flagged by this script, Cisco TAC must be contacted to identify and resolve the underlying issue before performing the upgrade.


### Missing presListener MO

In ACI there is a special class called presListener that handles policy download/deployment upon a Switch clean reload or upgrade.

Due to [CSCwn81692][62], after upgrade (or clean-reload) if the presListener MO is missing for a leaf , the infraAccPortP and infraAccBndlGrp MOs will fail to program in the interfaces, this puts the ports in "out-of-service" status.

If there any missing presListener, contact Cisco TAC to provide a workaround before an upgrade.


[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 @@ -2710,3 +2721,4 @@ If any instances of `configpushShardCont` are flagged by this script, Cisco TAC
[59]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwp95515
[60]: https://www.cisco.com/c/en/us/solutions/collateral/data-center-virtualization/application-centric-infrastructure/white-paper-c11-743951.html#Inter
[61]: https://www.cisco.com/c/en/us/solutions/collateral/data-center-virtualization/application-centric-infrastructure/white-paper-c11-743951.html#EnablePolicyCompression
[62]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwn81692
95 changes: 95 additions & 0 deletions tests/checks/pres_listener_mo_check/fabricNode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
[
{
"fabricNode": {
"attributes": {
"adSt": "off",
"address": "10.2.176.64",
"annotation": "",
"apicType": "apic",
"childAction": "",
"delayedHeartbeat": "no",
"dn": "topology/pod-1/node-101",
"extMngdBy": "",
"fabricSt": "active",
"id": "101",
"lastStateModTs": "2026-01-28T22:24:54.843+00:00",
"lcOwn": "local",
"modTs": "2026-01-28T22:25:42.207+00:00",
"model": "N9K-C93180YC-EX",
"monPolDn": "uni/fabric/monfab-default",
"name": "SITE2-L101",
"nameAlias": "",
"nodeType": "unspecified",
"role": "leaf",
"serial": "FDO204520TY",
"status": "",
"uid": "0",
"userdom": "all",
"vendor": "Cisco Systems, Inc",
"version": "n9000-15.2(6e)"
}
}
},
{
"fabricNode": {
"attributes": {
"adSt": "off",
"address": "10.2.176.66",
"annotation": "",
"apicType": "apic",
"childAction": "",
"delayedHeartbeat": "no",
"dn": "topology/pod-1/node-102",
"extMngdBy": "",
"fabricSt": "active",
"id": "102",
"lastStateModTs": "2026-01-28T22:25:45.863+00:00",
"lcOwn": "local",
"modTs": "2026-01-28T22:26:42.234+00:00",
"model": "N9K-C93180YC-EX",
"monPolDn": "uni/fabric/monfab-default",
"name": "SITE2-L102",
"nameAlias": "",
"nodeType": "unspecified",
"role": "leaf",
"serial": "FDO204520SZ",
"status": "",
"uid": "0",
"userdom": "all",
"vendor": "Cisco Systems, Inc",
"version": "n9000-15.2(6e)"
}
}
},
{
"fabricNode": {
"attributes": {
"adSt": "off",
"address": "10.2.176.67",
"annotation": "",
"apicType": "apic",
"childAction": "",
"delayedHeartbeat": "no",
"dn": "topology/pod-1/node-103",
"extMngdBy": "",
"fabricSt": "active",
"id": "103",
"lastStateModTs": "2026-01-28T22:25:45.863+00:00",
"lcOwn": "local",
"modTs": "2026-01-28T22:26:42.231+00:00",
"model": "N9K-C93240YC-FX2",
"monPolDn": "uni/fabric/monfab-default",
"name": "SITE2-L103",
"nameAlias": "",
"nodeType": "unspecified",
"role": "leaf",
"serial": "FDO25210JA1",
"status": "",
"uid": "0",
"userdom": "all",
"vendor": "Cisco Systems, Inc",
"version": "n9000-15.3(2f)"
}
}
}
]
38 changes: 38 additions & 0 deletions tests/checks/pres_listener_mo_check/presListener-Neg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[
{
"presListener": {
"attributes": {
"childAction": "",
"dn": "resregistry/resregistry-20/relnholder/rspresClass-[resregistry/resregistry-20/class-4307]/list-[uni/infra/nodecfgcont]",
"lcOwn": "local",
"lstDn": "uni/infra/nodecfgcont",
"modTs": "2026-01-21T01:44:22.201+00:00",
"status": ""
}
}
},
{
"presListener": {
"attributes": {
"childAction": "",
"dn": "resregistry/resregistry-20/relnholder/rspresClass-[resregistry/resregistry-20/class-4307]/list-[uni/infra/nodecfgcont/node-102]",
"lcOwn": "local",
"lstDn": "uni/infra/nodecfgcont/node-102",
"modTs": "2026-01-21T16:02:15.685+00:00",
"status": ""
}
}
},
{
"presListener": {
"attributes": {
"childAction": "",
"dn": "resregistry/resregistry-20/relnholder/rspresClass-[resregistry/resregistry-20/class-4307]/list-[uni/infra/nodecfgcont/node-101]",
"lcOwn": "local",
"lstDn": "uni/infra/nodecfgcont/node-101",
"modTs": "2026-01-21T16:02:15.685+00:00",
"status": ""
}
}
}
]
50 changes: 50 additions & 0 deletions tests/checks/pres_listener_mo_check/presListener-Pos.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[
{
"presListener": {
"attributes": {
"childAction": "",
"dn": "resregistry/resregistry-20/relnholder/rspresClass-[resregistry/resregistry-20/class-4307]/list-[uni/infra/nodecfgcont]",
"lcOwn": "local",
"lstDn": "uni/infra/nodecfgcont",
"modTs": "2026-01-21T01:44:22.201+00:00",
"status": ""
}
}
},
{
"presListener": {
"attributes": {
"childAction": "",
"dn": "resregistry/resregistry-20/relnholder/rspresClass-[resregistry/resregistry-20/class-4307]/list-[uni/infra/nodecfgcont/node-102]",
"lcOwn": "local",
"lstDn": "uni/infra/nodecfgcont/node-102",
"modTs": "2026-01-21T16:02:15.685+00:00",
"status": ""
}
}
},
{
"presListener": {
"attributes": {
"childAction": "",
"dn": "resregistry/resregistry-20/relnholder/rspresClass-[resregistry/resregistry-20/class-4307]/list-[uni/infra/nodecfgcont/node-101]",
"lcOwn": "local",
"lstDn": "uni/infra/nodecfgcont/node-101",
"modTs": "2026-01-21T16:02:15.685+00:00",
"status": ""
}
}
},
{
"presListener": {
"attributes": {
"childAction": "",
"dn": "resregistry/resregistry-20/relnholder/rspresClass-[resregistry/resregistry-20/class-4307]/list-[uni/infra/nodecfgcont/node-103]",
"lcOwn": "local",
"lstDn": "uni/infra/nodecfgcont/node-103",
"modTs": "2026-01-21T18:39:38.321+00:00",
"status": ""
}
}
}
]
67 changes: 67 additions & 0 deletions tests/checks/pres_listener_mo_check/test_pres_listener_mo_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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 = "pres_listener_mo_check"

# icurl queries
# fabricNode = "fabricNode.json"
# fabricNode += '?query-target-filter=and(eq(fabricNode.role,"leaf"),eq(fabricNode.fabricSt,"active"))'

presListener = "presListener.json"

@pytest.mark.parametrize(
"icurl_outputs, fabric_nodes, tversion, expected_result",
[
# No tVersion, MANUAL
(
{presListener: read_data(dir, "presListener-Pos.json")},
[],
None,
script.MANUAL
),
# FabricNodes missing, MANUAL
(
{presListener: read_data(dir, "presListener-Pos.json")},
[],
"6.1(3a)",
script.MANUAL
),
# tVersion newer than 6.1-3f , NA
(
{presListener: read_data(dir, "presListener-Pos.json")},
[],
"6.1(4a)",
script.NA
),
# PASS TESTS
# Version can be recovered with testapi (>=6.1-3f)
(
{presListener: read_data(dir, "presListener-Pos.json")},
read_data(dir, "fabricNode.json"),
"6.1(3a)",
script.PASS,
),
# FAIL_O TESTS
# Version can be recovered with testapi (>=6.1-3f)
(
{presListener: read_data(dir, "presListener-Neg.json")},
read_data(dir, "fabricNode.json"),
"6.1(3a)",
script.FAIL_O,
),
],
)
def test_logic(run_check, mock_icurl, fabric_nodes, tversion, expected_result):
result = run_check(
fabric_nodes=fabric_nodes,
tversion=script.AciVersion(tversion) if tversion else None,
)
assert result.result == expected_result