Skip to content
119 changes: 118 additions & 1 deletion aci-preupgrade-validation-script.py
Original file line number Diff line number Diff line change
Expand Up @@ -5970,6 +5970,121 @@ def configpush_shard_check(tversion, **kwargs):

return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url)

<<<<<<< HEAD
@check_wrapper(check_title="Disabled Cipher Configuration")
def disabled_cipher_check(tversion, username, password, fabric_nodes, **kwargs):
headers = ["APIC", "Disabled Cipher Count", "Nginx Log Check Status"]
data = []
recommended_action = "Re-enable the disabled ciphers or contact Cisco TAC for guidance on cipher configuration"
doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#disabled-cipher-configuration"

# Check 1: Verify target version is 6.0.2
if not tversion:
return Result(result=MANUAL, msg=TVER_MISSING)

if not (tversion.same_as("6.0(2a)") or tversion.same_as("6.0(2h)") or tversion.same_as("6.0(2j)")):
return Result(result=NA, msg=VER_NOT_AFFECTED)

# Check 2: Query for disabled ciphers
cipher_api = "commCipher.json?query-target-filter=and(or(wcard(commCipher.id,\"ECDHE-RSA\"),wcard(commCipher.id,\"DHE-RSA\"),wcard(commCipher.id,\"TLS_AES_256\")),eq(commCipher.state,\"disabled\"))"
try:
disabled_ciphers = icurl("class", cipher_api)
disabled_cipher_count = len(disabled_ciphers)

Choose a reason for hiding this comment

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

why do we need to use len in this case? please use totalcount value to validate.

Copy link
Author

@Harinadh-Saladi Harinadh-Saladi Dec 18, 2025

Choose a reason for hiding this comment

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

Hi Lovkesh, The entire codebase uses the same pattern - working directly with the array returned by icurl function.
Also I felt more efficient using length, since it doesn't needs extra parsing whereas totalcount needs extra parsing from string to int.

except Exception as e:
log.error("Failed to query disabled ciphers: %s", str(e))
return Result(result=ERROR, msg="Failed to query disabled ciphers: {}".format(str(e)), doc_url=doc_url)

if disabled_cipher_count == 0:
return Result(result=PASS, msg="No disabled ciphers found", doc_url=doc_url)

# Check 3: SSH to all APICs and check nginx logs
controllers = [node for node in fabric_nodes if node["fabricNode"]["attributes"]["role"] == "controller"]

if not controllers:
return Result(result=ERROR, msg="No controllers found in fabricNode. Is the cluster healthy?", doc_url=doc_url)

prints("")

for apic in controllers:
attr = apic["fabricNode"]["attributes"]
apic_name = attr["name"]
node_title = "Checking %s..." % apic_name
prints(node_title, end=" ")

try:
c = Connection(attr["address"])
c.username = username
c.password = password
c.log = LOG_FILE
c.connect()
except Exception as e:
log.error("Connection failed to %s: %s", apic_name, str(e))
data.append([apic_name, str(disabled_cipher_count), "Connection Error: {}".format(str(e))])
prints(ERROR)
continue

try:
cmd = "zgrep \"Failed to write nginxproxy conf file\" /var/log/dme/log/nginx.bin.war* 2>/dev/null | head -20"
c.cmd(cmd, timeout=35)

# Log raw output for debugging
log.debug("APIC %s raw output: %s", apic_name, repr(c.output))

# Check if output contains actual error messages
# If zgrep finds matches, the output will have error messages before the prompt
# If zgrep finds nothing, the output will only have the command echo and prompt
# Look for lines between command and prompt that contain the error message
lines = c.output.split("\n")
found_error = False

for line in lines:
# Skip the command echo line and prompt line
if "zgrep" in line or line.strip().endswith("#"):
continue
# If this line contains the error message and it's not empty, we found it
if line.strip() and "Failed to write nginxproxy conf file" in line:
found_error = True
log.debug("APIC %s found error line: %s", apic_name, repr(line))
break

if found_error:
data.append([apic_name, str(disabled_cipher_count), "FOUND"])
log.debug("APIC %s: Nginx error FOUND in logs", apic_name)
else:
data.append([apic_name, str(disabled_cipher_count), "Not found in nginx logs"])
log.debug("APIC %s: Nginx error NOT found in logs (only prompt returned)", apic_name)

prints(DONE)
except pexpect.TIMEOUT:
log.warning("Command timeout on %s", apic_name)
data.append([apic_name, str(disabled_cipher_count), "Command Timeout"])
prints(ERROR)
except pexpect.EOF:
log.warning("Connection closed unexpectedly on %s", apic_name)
data.append([apic_name, str(disabled_cipher_count), "Connection Closed"])
prints(ERROR)
except Exception as e:
log.exception("Error checking nginx logs on %s", apic_name)
data.append([apic_name, str(disabled_cipher_count), "Error: {}".format(str(e))])
prints(ERROR)
finally:
try:
c.close()
except Exception:
pass

# Determine final result based on priority: FAIL_O > ERROR > PASS
if not data:
return Result(result=ERROR, msg="Unable to check nginx logs on any APIC", headers=headers, data=[], doc_url=doc_url)

if any("FOUND" in row[2] for row in data):
result = FAIL_O
elif any("Error" in row[2] or "Timeout" in row[2] or "Closed" in row[2] for row in data):
result = ERROR
else:
result = PASS
return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url)
=======

@check_wrapper(check_title='APIC VMM inventory sync fault (F0132)')
def apic_vmm_inventory_sync_faults_check(**kwargs):
Expand Down Expand Up @@ -6006,6 +6121,7 @@ def apic_vmm_inventory_sync_faults_check(**kwargs):
unformatted_data=unformatted_data,
recommended_action=recommended_action,
doc_url=doc_url)
>>>>>>> upstream/master

# ---- Script Execution ----

Expand Down Expand Up @@ -6168,6 +6284,7 @@ class CheckManager:
standby_sup_sync_check,
isis_database_byte_check,
configpush_shard_check,
disabled_cipher_check,

]
ssh_checks = [
Expand Down Expand Up @@ -6312,7 +6429,7 @@ def main(_args=None):
# Print result of each failed check
for index, check_id in enumerate(cm.check_ids):
result_obj = cm.get_check_result(check_id)
if not result_obj or result_obj.result in (NA, PASS):
if not result_obj or result_obj.result in (NA,PASS):
continue
check_title = cm.get_check_title(check_id)
print_result(index + 1, cm.total_checks, check_title, **result_obj.as_dict())
Expand Down
17 changes: 17 additions & 0 deletions docs/docs/validations.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,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: |
[Disabled Cipher Configuration][d29] | CSCwf26626 | :white_check_mark: |

[d1]: #ep-announce-compatibility
[d2]: #eventmgr-db-size-defect-susceptibility
Expand Down Expand Up @@ -220,6 +221,8 @@ Items | Defect | This Script
[d26]: #stale-pconsra-object
[d27]: #isis-dteps-byte-size
[d28]: #policydist-configpushshardcont-crash
[d29]: #disabled-cipher-check



## General Check Details
Expand Down Expand Up @@ -2614,6 +2617,17 @@ 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.


### Disabled cipher configuration
RCA:
After upgrading the APIC to an affected version it falls back to bringup UI wizard.This issue occurs because of all HTTPS/SSL ciphers were disabled under uni/fabric/comm-default/https path and in nginx or nginxproxy under /data/nginx/conf/nginx.conf

IMPACT:
If all HTTPS/SSL ciphers are disabled before APIC upgrade then it falls back to bringup UI wizard.

SUGGESTION:
If you are trying to upgrade the APIC to 6.0.x release, in order to avoid this issue, ensure that ciphers like 'EECDH', 'ECDSA', 'EECDH+aRSA+SHA256', 'EECDH+aRSA+SHA384' remain enabled prior to 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 @@ -2676,3 +2690,6 @@ 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/CSCwf26626


Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"totalCount": "2",
"imdata": [
{
"commCipher": {
"attributes": {
"annotation": "",
"childAction": "",
"configurable": "yes",
"dn": "uni/fabric/comm-default/https/cph-ECDHE-RSA-AES256-SHA384",
"extMngdBy": "",
"id": "ECDHE-RSA-AES256-SHA384",
"lcOwn": "local",
"modTs": "2025-12-09T12:07:57.857+00:00",
"state": "disabled",
"status": "",
"uid": "0",
"userdom": "all",
"weak": "yes"
}
}
},
{
"commCipher": {
"attributes": {
"annotation": "",
"childAction": "",
"configurable": "yes",
"dn": "uni/fabric/comm-default/https/cph-ECDHE-RSA-AES128-SHA256",
"extMngdBy": "",
"id": "ECDHE-RSA-AES128-SHA256",
"lcOwn": "local",
"modTs": "2025-12-09T12:07:57.857+00:00",
"state": "disabled",
"status": "",
"uid": "0",
"userdom": "all",
"weak": "yes"
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"totalCount": "0",
"imdata": []
}
39 changes: 39 additions & 0 deletions tests/checks/disabled_cipher_check/fabricNode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[
{
"fabricNode": {
"attributes": {
"dn": "topology/pod-1/node-1",
"address": "10.0.0.1",
"fabricSt": "active",
"id": "1",
"name": "apic1",
"role": "controller"
}
}
},
{
"fabricNode": {
"attributes": {
"dn": "topology/pod-1/node-2",
"address": "10.0.0.2",
"fabricSt": "active",
"id": "2",
"name": "apic2",
"role": "controller"
}
}
},
{
"fabricNode": {
"attributes": {
"dn": "topology/pod-1/node-3",
"address": "10.0.0.3",
"fabricSt": "active",
"id": "3",
"name": "apic3",
"role": "controller"
}
}
}
]

Loading