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
73 changes: 73 additions & 0 deletions aci-preupgrade-validation-script.py
Original file line number Diff line number Diff line change
Expand Up @@ -4120,6 +4120,78 @@ def apic_ca_cert_validation(**kwargs):
return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url)


@check_wrapper(check_title="Spine/Leaf Cert Validation")
def switch_cert_validation(fabric_nodes, **kwargs):
result = FAIL_O
headers = ["Pod-ID", "Node-ID", "Node Name", "Role", "Expiry Date"]
data = []
recommended_action = "Renew the expired node SSL certificate before proceeding with the upgrade."

if not fabric_nodes:
return Result(result=PASS)

switch_nodes = {}
for fn in fabric_nodes:
fn_attrs = fn.get('fabricNode', {}).get('attributes', {})
role = fn_attrs.get('role')
if role in ['spine', 'leaf']:
node_id = fn_attrs.get('id')
if node_id:
switch_nodes[node_id] = fn_attrs

if not switch_nodes:
return Result(result=PASS)

pki_certs = icurl('class', 'pkiFabricNodeSSLCertificate.json')
if not pki_certs:
return Result(result=PASS)

for pki_cert in pki_certs:
attrs = pki_cert.get('pkiFabricNodeSSLCertificate', {}).get('attributes', {})
node_id = attrs.get('nodeId')
if not node_id or node_id not in switch_nodes:
continue

node_info = switch_nodes[node_id]
node_name = node_info.get('name', 'N/A')
role = node_info.get('role', 'N/A')
dn_match = re.search(node_regex, node_info.get('dn', ''))
pod_id = dn_match.group('pod') if dn_match else 'N/A'

not_after_str = attrs.get('validityNotAfter', '')
if not not_after_str:
log.debug("node %s: empty validityNotAfter, skipping", node_id)
continue

try:
expiry_dt = datetime.strptime(not_after_str[:19], "%Y-%m-%dT%H:%M:%S")
except ValueError:
log.debug("node %s: cannot parse validityNotAfter=%r, skipping",
node_id, not_after_str)
continue

if expiry_dt > datetime.utcnow():
continue

data.append([
pod_id,
node_id,
node_name,
role,
not_after_str,
])

if not data:
result = PASS

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


@check_wrapper(check_title="FabricDomain Name")
def fabricdomain_name_check(cversion, tversion, fabric_nodes, **kwargs):
result = FAIL_O
Expand Down Expand Up @@ -6432,6 +6504,7 @@ class CheckManager:
fabric_link_redundancy_check,
apic_downgrade_compat_warning_check,
svccore_excessive_data_check,
switch_cert_validation,

# Faults
apic_disk_space_faults_check,
Expand Down
9 changes: 8 additions & 1 deletion docs/docs/validations.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Items | This Script
[APIC Database Size][g18] | :white_check_mark: | :no_entry_sign:
[APIC downgrade compatibility when crossing 6.2 release][g19]| :white_check_mark: | :no_entry_sign:
[Svccore Excessive Data Check][g20] | :white_check_mark: | :no_entry_sign:
[Spine/Leaf Cert Validation][g21] | :white_check_mark: | :no_entry_sign:

[g1]: #compatibility-target-aci-version
[g2]: #compatibility-cimc-version
Expand All @@ -59,6 +60,7 @@ Items | This Script
[g18]: #apic-database-size
[g19]: #apic-downgrade-compatibility-when-crossing-62-release
[g20]: #svccore-excessive-data-check
[g21]: #spineleaf-cert-validation

### Fault Checks
Items | Faults | This Script | APIC built-in
Expand Down Expand Up @@ -2785,6 +2787,11 @@ Administrators may be unable to access or operate the APIC GUI, potentially impa
This check will verify the count of the `svccoreCtrlr` Managed Object and raise and alarm with the bug if object count found more than 240. Remove the content or objects of `svccoreCtrlr` or `svccoreNode`. Contact Cisco TAC or upgrade to a release containing the fix for CSCws84232 before proceeding with an upgrade.


### Spine/Leaf Cert Validation

The script checks the expiration date of SSL certificates on spine and leaf switches (nodes with roles `spine` or `leaf`) via the `pkiFabricNodeSSLCertificate` class. If any certificate has already expired, it will be highlighted. Expired node certificates can prevent switches from successfully joining the fabric or communicating securely with the APIC controllers.


[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 @@ -2854,4 +2861,4 @@ This check will verify the count of the `svccoreCtrlr` Managed Object and raise
[66]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwr66848
[67]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwh80837
[68]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwd40071
[69]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCws84232
[69]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCws84232
38 changes: 38 additions & 0 deletions tests/checks/switch_cert_validation/fabricNode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[
{
"fabricNode": {
"attributes": {
"dn": "topology/pod-1/node-1",
"fabricSt": "commissioned",
"id": "1",
"name": "apic1",
"role": "controller",
"nodeType": "unspecified"
}
}
},
{
"fabricNode": {
"attributes": {
"dn": "topology/pod-1/node-101",
"fabricSt": "active",
"id": "101",
"name": "leaf101",
"role": "leaf",
"nodeType": "unspecified"
}
}
},
{
"fabricNode": {
"attributes": {
"dn": "topology/pod-1/node-102",
"fabricSt": "active",
"id": "102",
"name": "spine102",
"role": "spine",
"nodeType": "unspecified"
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[
{
"pkiFabricNodeSSLCertificate": {
"attributes": {
"nodeId": "101",
"validCertificate": "yes",
"validityNotAfter": "2035-05-26T19:19:27.000+00:00",
"validityNotBefore": "2015-05-26T19:19:27.000+00:00",
"message": "",
"subject": "/CN=SAL1948U33K",
"serialNumber": "SAL1948U33K"
}
}
},
{
"pkiFabricNodeSSLCertificate": {
"attributes": {
"nodeId": "102",
"validCertificate": "yes",
"validityNotAfter": "2035-05-26T19:19:27.000+00:00",
"validityNotBefore": "2015-05-26T19:19:27.000+00:00",
"message": "",
"subject": "/CN=SAL1948U35D",
"serialNumber": "SAL1948U35D"
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[
{
"pkiFabricNodeSSLCertificate": {
"attributes": {
"nodeId": "1",
"validCertificate": "yes",
"validityNotAfter": "2020-01-01T00:00:00.000+00:00",
"validityNotBefore": "2010-01-01T00:00:00.000+00:00",
"message": "",
"subject": "/CN=APIC1",
"serialNumber": "APIC1SERIAL"
}
}
},
{
"pkiFabricNodeSSLCertificate": {
"attributes": {
"nodeId": "101",
"validCertificate": "no",
"validityNotAfter": "2099-05-26T19:19:27.000+00:00",
"validityNotBefore": "2019-05-26T19:19:27.000+00:00",
"message": "Failed to parse the subject line as a valid ACI fabric certificate AND invalid serial",
"subject": "/CN=SAL1948U33K",
"serialNumber": "SAL1948U33K"
}
}
},
{
"pkiFabricNodeSSLCertificate": {
"attributes": {
"nodeId": "102",
"validCertificate": "yes",
"validityNotAfter": "2020-01-01T00:00:00.000+00:00",
"validityNotBefore": "2010-01-01T00:00:00.000+00:00",
"message": "",
"subject": "/CN=SAL1948U35D",
"serialNumber": "SAL1948U35D"
}
}
},
{
"pkiFabricNodeSSLCertificate": {
"attributes": {
"nodeId": "999",
"validCertificate": "yes",
"validityNotAfter": "2020-01-01T00:00:00.000+00:00",
"validityNotBefore": "2010-01-01T00:00:00.000+00:00",
"message": "",
"subject": "/CN=GHOST",
"serialNumber": "GHOST"
}
}
}
]
80 changes: 80 additions & 0 deletions tests/checks/switch_cert_validation/test_switch_cert_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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 = "switch_cert_validation"

# icurl queries
pki_certs_api = "pkiFabricNodeSSLCertificate.json"


@pytest.mark.parametrize(
"icurl_outputs, fabric_nodes, expected_result, expected_data",
[
# FAIL - Only expired certs on spine/leaf are flagged.
# The positive fixture contains 4 certs: an expired APIC controller cert
# (must be ignored - controllers are out of scope), a valid leaf cert
# (not expired), an expired spine cert (must be flagged), and a cert for
# an unknown nodeId (must be ignored).
(
{pki_certs_api: read_data(dir, "pkiFabricNodeSSLCertificate_positive.json")},
read_data(dir, "fabricNode.json"),
script.FAIL_O,
[
[
"1",
"102",
"spine102",
"spine",
"2020-01-01T00:00:00.000+00:00",
],
],
),
# PASS - All certificates are valid and unexpired.
(
{pki_certs_api: read_data(dir, "pkiFabricNodeSSLCertificate_negative.json")},
read_data(dir, "fabricNode.json"),
script.PASS,
[],
),
# PASS - pkiFabricNodeSSLCertificate class returns no objects.
(
{pki_certs_api: []},
read_data(dir, "fabricNode.json"),
script.PASS,
[],
),
# PASS - fabric_nodes contains controllers only; even with an expired
# controller cert in the response, the check must return PASS because
# it is scoped to spine/leaf nodes only.
(
{pki_certs_api: read_data(dir, "pkiFabricNodeSSLCertificate_positive.json")},
[
fn for fn in read_data(dir, "fabricNode.json")
if fn["fabricNode"]["attributes"].get("role") == "controller"
],
script.PASS,
[],
),
# PASS - fabric_nodes is empty (no APIC data at all).
(
{pki_certs_api: read_data(dir, "pkiFabricNodeSSLCertificate_positive.json")},
[],
script.PASS,
[],
),
],
)
def test_logic(run_check, mock_icurl, fabric_nodes, expected_result, expected_data):
result = run_check(
fabric_nodes=fabric_nodes,
)
assert result.result == expected_result
assert result.data == expected_data