From 5f991ad4624674c1330199ef1f6380323c2fe196 Mon Sep 17 00:00:00 2001 From: Giovanni Meo Date: Fri, 11 Sep 2026 09:09:47 -0700 Subject: [PATCH] Implement validation to suggest to run 3 APIC clusters - Starting from 6.3(1) our recommendation is to run 3 APIC cluster with APIC-SERVER-G5 skews for improved resilience. --- aci-preupgrade-validation-script.py | 59 ++++++++++++++ docs/docs/validations.md | 9 +++ .../test_apic_cluster_size_check.py | 77 +++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 tests/checks/apic_cluster_size_check/test_apic_cluster_size_check.py diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 094f0e7..4ba3083 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -85,6 +85,22 @@ ("6.1(5)", "apicl4"): CIMC_RELEASE_NOTE_SUPPORT_615_M6, ("6.1(5)", "apicm4"): CIMC_RELEASE_NOTE_SUPPORT_615_M6, } +# APIC models exempt from the three-node cluster size limit. Keep this +# embedded so the standalone validation script has no external dependency. +APIC_EXCLUDED_MODELS = ( + 'APIC-SERVER-L1', + 'APIC-SERVER-L2', + 'APIC-SERVER-L3', + 'APIC-SERVER-L4', + 'APIC-SERVER-L3T', + 'APIC-SERVER-L4T', + 'APIC-SERVER-M1', + 'APIC-SERVER-M2', + 'APIC-SERVER-M3', + 'APIC-SERVER-M4', + 'APIC-SERVER-M3T', + 'APIC-SERVER-M4T', +) # regex constants node_regex = r'topology/pod-(?P\d+)/node-(?P\d+)' port_regex = node_regex + r'/sys/phys-\[(?P.+)\]' @@ -1771,6 +1787,11 @@ def get_fabric_nodes(): return fabricNodes +def get_apic_excluded_models(): + """Return APIC models exempt from the three-node cluster size limit.""" + return set(APIC_EXCLUDED_MODELS) + + def get_current_versions(fabric_nodes, arg_cversion): """ Returns: AciVersion instances of APIC and lowest switch """ if arg_cversion: @@ -6397,6 +6418,43 @@ 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='APIC Cluster Size') +def apic_cluster_size_check(tversion, **kwargs): + """Warn when an APIC cluster has more than three non-excluded models.""" + apic_api = ('fabricNode.json?query-target-filter=' + 'and(eq(fabricNode.role,"controller"),' + 'eq(fabricNode.apicType,"apic"),' + 'eq(fabricNode.fabricSt,"commissioned"))') + headers = ["Node ID", "Node Name", "Model", "Excluded from 3-Node Limit"] + recommended_action = ('Reduce the APIC cluster to three nodes before continuing ' + 'the upgrade. Clusters larger than three nodes are not ' + 'supported for this upgrade unless they include an excluded ' + 'APIC model.') + doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#apic-cluster-size' + + if not tversion: + return Result(result=MANUAL, msg=TVER_MISSING) + if tversion.older_than("6.3(1a)"): + return Result(result=NA, msg=VER_NOT_AFFECTED, doc_url=doc_url) + + apics = icurl('class', apic_api) + excluded_models = get_apic_excluded_models() + models = [node.get('fabricNode', {}).get('attributes', {}).get('model', '') + for node in apics] + + if len(apics) <= 3 or any(model in excluded_models for model in models): + return Result(result=PASS, msg="APIC cluster size and models are supported.") + + data = [] + for node in apics: + attributes = node.get('fabricNode', {}).get('attributes', {}) + model = attributes.get('model', '') + data.append([attributes.get('id', ''), attributes.get('name', ''), model, + 'yes' if model in excluded_models else 'no']) + return Result(result=MANUAL, msg="APIC clusters require no more than 3 nodes unless an excluded model is present.", + headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) + + @check_wrapper(check_title='Auto Firmware Update on Switch Discovery') def auto_firmware_update_on_switch_check(cversion, tversion, **kwargs): result = PASS @@ -7081,6 +7139,7 @@ class CheckManager: validate_32_64_bit_image_check, fabric_link_redundancy_check, apic_downgrade_compat_warning_check, + apic_cluster_size_check, svccore_excessive_data_check, # Faults diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 717c175..975f7f4 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -37,6 +37,7 @@ Items | This Script [Fabric Link Redundancy][g17] | :white_check_mark: | :no_entry_sign: [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: +[APIC Cluster Size][g22] | :white_check_mark: | :no_entry_sign: [Supported Hardware Compatibility][g20] | :white_check_mark: | :no_entry_sign: [Svccore Excessive Data Check][g21] | :white_check_mark: | :no_entry_sign: @@ -61,6 +62,7 @@ Items | This Script [g19]: #apic-downgrade-compatibility-when-crossing-62-release [g20]: #supported-hardware-compatibility [g21]: #svccore-excessive-data-check +[g22]: #apic-cluster-size ### Fault Checks Items | Faults | This Script | APIC built-in @@ -569,6 +571,13 @@ This check alerts you if you are crossing the 6.2 boundary, beyond which downgra If it's for a lab environment, you can initialize the fabric and perform a fresh ISO installation of pre-6.2(1) on APICs. +### APIC Cluster Size + +For target versions 6.3(1a) and later, APIC clusters must contain no more than three commissioned APIC controllers to continue an upgrade unless they include an APIC-SERVER-L1 through L4 or APIC-SERVER-M1 through M4 model. This validation queries commissioned APIC controllers and compares their models with the excluded models embedded in the validation script. + +If more than three controllers are found and none is one of the excluded models, reduce the cluster to three nodes before continuing the upgrade. + + ## Fault Check Details ### APIC Disk Space Usage diff --git a/tests/checks/apic_cluster_size_check/test_apic_cluster_size_check.py b/tests/checks/apic_cluster_size_check/test_apic_cluster_size_check.py new file mode 100644 index 0000000..66fcd4b --- /dev/null +++ b/tests/checks/apic_cluster_size_check/test_apic_cluster_size_check.py @@ -0,0 +1,77 @@ +import importlib + +import pytest + +script = importlib.import_module("aci-preupgrade-validation-script") + +test_function = "apic_cluster_size_check" +apic_api = ('fabricNode.json?query-target-filter=' + 'and(eq(fabricNode.role,"controller"),' + 'eq(fabricNode.apicType,"apic"),' + 'eq(fabricNode.fabricSt,"commissioned"))') + + +def fabric_nodes(models): + return [{"fabricNode": {"attributes": { + "id": str(index + 1), "name": "apic{}".format(index + 1), "model": model, + }}} for index, model in enumerate(models)] + + +@pytest.mark.parametrize( + "models, expected_result", + [ + (["APIC-SERVER-G5"] * 3, script.PASS), + (["APIC-SERVER-G5"] * 4, script.MANUAL), + (["APIC-SERVER-G5"] * 4 + ["APIC-SERVER-L2"], script.PASS), + (["APIC-SERVER-L1"] * 4, script.PASS), + (["APIC-SERVER-M2"] * 4, script.PASS), + (["APIC-SERVER-M1"] * 4, script.PASS), + (["APIC-SERVER-L4T"] * 4, script.PASS), + (["APIC-SERVER-M4T"] * 4, script.PASS), + (["APIC-SERVER-G5"] * 4, script.MANUAL), + (["APIC-SERVER-G5T"] * 4, script.MANUAL), + ], +) +def test_logic(monkeypatch, models, expected_result): + monkeypatch.setattr(script, "icurl", lambda apitype, query: fabric_nodes(models)) + result = script.apic_cluster_size_check( + tversion=script.AciVersion("6.3(1a)"), + finalize_check=lambda check_id, result: None, + ) + assert result.result == expected_result + + +def test_reports_models_and_recommended_action(monkeypatch): + monkeypatch.setattr(script, "icurl", lambda apitype, query: fabric_nodes(["APIC-SERVER-G5"] * 4)) + result = script.apic_cluster_size_check( + tversion=script.AciVersion("6.3(1a)"), + finalize_check=lambda check_id, result: None, + ) + assert result.headers == ["Node ID", "Node Name", "Model", "Excluded from 3-Node Limit"] + assert result.data == [ + [str(index), "apic{}".format(index), "APIC-SERVER-G5", "no"] + for index in range(1, 5) + ] + assert "Reduce the APIC cluster to three nodes" in result.recommended_action + + +@pytest.mark.parametrize("target_version", ["6.2(7f)", "6.3(0a)"]) +def test_older_targets_are_not_blocked(monkeypatch, target_version): + def unexpected_api_call(*args): + pytest.fail("APIC cluster size should not be checked for this target") + + monkeypatch.setattr(script, "icurl", unexpected_api_call) + result = script.apic_cluster_size_check( + tversion=script.AciVersion(target_version), + finalize_check=lambda check_id, result: None, + ) + assert result.result == script.NA + + +def test_missing_target_version_is_manual(): + result = script.apic_cluster_size_check( + tversion=None, + finalize_check=lambda check_id, result: None, + ) + assert result.result == script.MANUAL + assert result.msg == script.TVER_MISSING