From b0bea57b9f70d7324d404f58df9029af80f40764 Mon Sep 17 00:00:00 2001 From: Priyanka Date: Mon, 27 Jul 2026 10:46:00 +0530 Subject: [PATCH 1/5] Added APIC OOB connectivity checks for CSCwu91693 --- aci-preupgrade-validation-script.py | 98 ++++++++++++++ docs/docs/validations.md | 23 ++++ .../commHttps_custom_port.json | 9 ++ .../commHttps_default_port.json | 9 ++ .../test_apic_oob_custom_port_check.py | 124 ++++++++++++++++++ .../test_apic_oob_default_port_check.py | 100 ++++++++++++++ .../topSystem_3apics_oob.json | 32 +++++ .../topSystem_no_oob.json | 12 ++ 8 files changed, 407 insertions(+) create mode 100644 tests/checks/apic_oob_connectivity_check/commHttps_custom_port.json create mode 100644 tests/checks/apic_oob_connectivity_check/commHttps_default_port.json create mode 100644 tests/checks/apic_oob_connectivity_check/test_apic_oob_custom_port_check.py create mode 100644 tests/checks/apic_oob_connectivity_check/test_apic_oob_default_port_check.py create mode 100644 tests/checks/apic_oob_connectivity_check/topSystem_3apics_oob.json create mode 100644 tests/checks/apic_oob_connectivity_check/topSystem_no_oob.json diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 5e79f56..a57c468 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6702,6 +6702,102 @@ def stale_dbgacEpgSummaryTask_check(tversion, **kwargs): return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) +def _get_apic_oob_connectivity(topSystems, port): + """Helper to test OOB TCP connectivity to all APIC controllers on a given port. + + Returns: + data (list): rows of [node_id, ip, port, "Unreachable"] for unreachable peers + has_error (bool): True if an unexpected exception occurred during connectivity check + """ + data = [] + has_error = False + + for topSystem in topSystems: + attrs = topSystem['topSystem']['attributes'] + 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: + continue + + try: + if subprocess.call( + 'curl --max-time 5 -k -s -o /dev/null https://{}:{} 2>/dev/null'.format(ip, port), + shell=True + ) in [7, 28]: + data.append([node_id, ip, port, "Unreachable"]) + except Exception as e: + log.error("Exception checking OOB connectivity for node %s: %s", node_id, e) + has_error = True + continue + + return data, has_error + + +@check_wrapper(check_title="APIC OOB Connectivity - Default Port") +def apic_oob_default_port_check(tversion, **kwargs): + result = PASS + headers = ["Node ID", "OOB IP", "Port", "Status"] + recommended_action = "Restore OOB management connectivity between all APICs and ensure TCP port 443 is reachable across the OOB network." + doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#apic-oob-connectivity---default-port' + + if not tversion: + return Result(result=MANUAL, msg=TVER_MISSING) + + if tversion.older_than("6.0(2a)"): + return Result(result=NA, msg=VER_NOT_AFFECTED) + + topSystems = icurl('class', 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")') + if not topSystems: + return Result(result=NA, msg="No APIC controller nodes found.") + + data, has_error = _get_apic_oob_connectivity(topSystems, 443) + + if has_error: + result = ERROR + elif data: + result = FAIL_O + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) + + +@check_wrapper(check_title="APIC OOB Connectivity - Custom HTTPS Port") +def apic_oob_custom_port_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 configured HTTPS port from commHttps is reachable across the OOB network." + doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#apic-oob-connectivity---custom-https-port' + + if not tversion: + return Result(result=MANUAL, msg=TVER_MISSING) + + if cversion.older_than("6.2(1a)") or tversion.older_than("6.2(1a)"): + return Result(result=NA, msg=VER_NOT_AFFECTED) + + port = 443 + commHttps = icurl('class', 'commHttps.json') + 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 commHttps HTTPS port from commHttps MO.") + + topSystems = icurl('class', 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")') + if not topSystems: + return Result(result=NA, msg="No APIC controller nodes found.") + + data, has_error = _get_apic_oob_connectivity(topSystems, port) + + 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 ---- @@ -6877,6 +6973,8 @@ class CheckManager: wred_affected_model_check, n9k_c93180yc_fx3_switch_memory_check, stale_dbgacEpgSummaryTask_check, + apic_oob_default_port_check, + apic_oob_custom_port_check, ] ssh_checks = [ diff --git a/docs/docs/validations.md b/docs/docs/validations.md index f788681..b5f7b9e 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -206,6 +206,8 @@ Items | Defect | This Script [WRED with Affected FM Models][d35] | CSCwt50713 | :white_check_mark: | :no_entry_sign: [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: +[APIC OOB Connectivity - Default Port][d38] | CSCwu91693 | :white_check_mark: | :white_check_mark: 6.2(2) +[APIC OOB Connectivity - Custom HTTPS Port][d39] | CSCwu91693 | :white_check_mark: | :white_check_mark: 6.2(2) [d1]: #ep-announce-compatibility [d2]: #eventmgr-db-size-defect-susceptibility @@ -244,6 +246,8 @@ Items | Defect | This Script [d35]: #wred-with-affected-fm-models [d36]: #n9k-c93180yc-fx3-switch-memory-less-than-32gb [d37]: #stale-dbgacepgsummarytask-objects +[d38]: #apic-oob-connectivity---default-port +[d39]: #apic-oob-connectivity---custom-https-port ## General Check Details @@ -2846,6 +2850,24 @@ Affected versions: 6.1(5e) and below, or 6.2(1g). Contact Cisco TAC for next steps. For more details, refer to the workaround in [CSCwt69100][75]. +### APIC OOB Connectivity - Default Port + +Due to [CSCwu91693][77], when an APIC cluster upgrade is triggered, the orchestrating APIC fans out an HTTPS POST to every peer APIC over the OOB management network on the default port 443 (used by bootx starting from 6.0(2)). If OOB connectivity to any peer APIC is broken at upgrade time, only the reachable APICs receive the trigger and start upgrading. The unreachable APICs are silently skipped, leaving the cluster partially upgraded — a state that cannot be recovered remotely. + +This check queries `topSystem` filtered to `role=controller` to collect all APIC OOB management IPs and attempts a TCP connection on port 443 with a 5-second timeout. If any peer is unreachable, the check fails. + +Applicable when target version is 6.0(2) or above. + + +### APIC OOB Connectivity - Custom HTTPS Port + +Due to [CSCwu91693][77], the APIC upgrade fanout uses the configured HTTPS port from the `commHttps` policy (default 443) to send the upgrade begin request to peer APICs over the OOB management network. If OOB connectivity on this port is broken for any peer, only a subset of APICs start upgrading, leaving the cluster in a partially upgraded and unrecoverable state. + +This check queries `commHttps` for the configured HTTPS port, queries `topSystem` filtered to `role=controller` for all APIC OOB management IPs, and attempts a TCP connection on the configured port with a 5-second timeout. If any peer is unreachable, the check fails. + +Applicable when both current version and target version are 6.2 or above. + + [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 @@ -2923,4 +2945,5 @@ Contact Cisco TAC for next steps. For more details, refer to the workaround in [ [74]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwm42741 [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/CSCwu91693 diff --git a/tests/checks/apic_oob_connectivity_check/commHttps_custom_port.json b/tests/checks/apic_oob_connectivity_check/commHttps_custom_port.json new file mode 100644 index 0000000..1ce3911 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/commHttps_custom_port.json @@ -0,0 +1,9 @@ +[ + { + "commHttps": { + "attributes": { + "port": "8443" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/commHttps_default_port.json b/tests/checks/apic_oob_connectivity_check/commHttps_default_port.json new file mode 100644 index 0000000..5c99dc5 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/commHttps_default_port.json @@ -0,0 +1,9 @@ +[ + { + "commHttps": { + "attributes": { + "port": "443" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/test_apic_oob_custom_port_check.py b/tests/checks/apic_oob_connectivity_check/test_apic_oob_custom_port_check.py new file mode 100644 index 0000000..baa0f36 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/test_apic_oob_custom_port_check.py @@ -0,0 +1,124 @@ +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 = "apic_oob_custom_port_check" + +# icurl queries +topSystem = 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")' +commHttps = "commHttps.json" + + +@pytest.mark.parametrize( + "icurl_outputs, cversion, tversion, curl_exit_codes, expected_result", + [ + # tversion not provided -> MANUAL + ( + {topSystem: [], commHttps: []}, + "6.2(1a)", + None, + [], + script.MANUAL, + ), + # cversion < 6.2(1a) -> NA (version not affected) + ( + {topSystem: [], commHttps: []}, + "6.0(3a)", + "6.2(1a)", + [], + script.NA, + ), + # tversion < 6.2(1a) -> NA (version not affected) + ( + {topSystem: [], commHttps: []}, + "6.2(1a)", + "6.0(3a)", + [], + script.NA, + ), + # Both >= 6.2, no controller nodes found -> NA + ( + {topSystem: [], commHttps: read_data(dir, "commHttps_default_port.json")}, + "6.2(1a)", + "6.2(2a)", + [], + script.NA, + ), + # Both >= 6.2, all APICs have no OOB configured -> PASS + ( + { + topSystem: read_data(dir, "topSystem_no_oob.json"), + commHttps: read_data(dir, "commHttps_default_port.json"), + }, + "6.2(1a)", + "6.2(2a)", + [], + script.PASS, + ), + # Both >= 6.2, default port 443, all APICs reachable -> PASS + ( + { + topSystem: read_data(dir, "topSystem_3apics_oob.json"), + commHttps: read_data(dir, "commHttps_default_port.json"), + }, + "6.2(1a)", + "6.2(2a)", + [0, 0, 0], + script.PASS, + ), + # Both >= 6.2, default port 443, one APIC unreachable (exit code 28) -> FAIL_UF + ( + { + topSystem: read_data(dir, "topSystem_3apics_oob.json"), + commHttps: read_data(dir, "commHttps_default_port.json"), + }, + "6.2(1a)", + "6.2(2a)", + [0, 28, 0], + script.FAIL_UF, + ), + # Both >= 6.2, custom port 8443, one APIC unreachable (exit code 7) -> FAIL_UF + ( + { + topSystem: read_data(dir, "topSystem_3apics_oob.json"), + commHttps: read_data(dir, "commHttps_custom_port.json"), + }, + "6.2(1a)", + "6.2(2a)", + [0, 7, 0], + script.FAIL_UF, + ), + # Both >= 6.2, custom port 8443, all APICs unreachable -> FAIL_UF + ( + { + topSystem: read_data(dir, "topSystem_3apics_oob.json"), + commHttps: read_data(dir, "commHttps_custom_port.json"), + }, + "6.2(1a)", + "6.2(2a)", + [28, 28, 28], + script.FAIL_UF, + ), + ], +) +def test_logic(run_check, mock_icurl, monkeypatch, cversion, tversion, curl_exit_codes, expected_result): + idx = [0] + + def mock_subprocess_call(cmd, shell=False): + code = curl_exit_codes[idx[0]] if idx[0] < len(curl_exit_codes) else 0 + idx[0] += 1 + return code + + monkeypatch.setattr(script.subprocess, "call", mock_subprocess_call) + result = run_check( + cversion=script.AciVersion(cversion), + tversion=script.AciVersion(tversion) if tversion else None, + ) + assert result.result == expected_result diff --git a/tests/checks/apic_oob_connectivity_check/test_apic_oob_default_port_check.py b/tests/checks/apic_oob_connectivity_check/test_apic_oob_default_port_check.py new file mode 100644 index 0000000..5da79e8 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/test_apic_oob_default_port_check.py @@ -0,0 +1,100 @@ +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 = "apic_oob_default_port_check" + +# icurl queries +topSystem = 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")' + + +@pytest.mark.parametrize( + "icurl_outputs, cversion, tversion, curl_exit_codes, expected_result", + [ + # tversion not provided -> MANUAL + ( + {topSystem: []}, + "6.0(2a)", + None, + [], + script.MANUAL, + ), + # tversion < 6.0(2a) -> NA (version not affected) + ( + {topSystem: []}, + "5.2(7f)", + "5.2(7f)", + [], + script.NA, + ), + # tversion >= 6.0(2a), no controller nodes found -> NA + ( + {topSystem: []}, + "6.0(2a)", + "6.0(3a)", + [], + script.NA, + ), + # tversion >= 6.0(2a), all APICs have no OOB configured -> PASS + ( + {topSystem: read_data(dir, "topSystem_no_oob.json")}, + "6.0(2a)", + "6.0(3a)", + [], + script.PASS, + ), + # tversion >= 6.0(2a), all APICs reachable (curl returns 0) -> PASS + ( + {topSystem: read_data(dir, "topSystem_3apics_oob.json")}, + "6.0(2a)", + "6.0(3a)", + [0, 0, 0], + script.PASS, + ), + # tversion >= 6.0(2a), one APIC unreachable (exit code 28 - timeout) -> FAIL_O + ( + {topSystem: read_data(dir, "topSystem_3apics_oob.json")}, + "6.0(2a)", + "6.0(3a)", + [0, 28, 0], + script.FAIL_O, + ), + # tversion >= 6.0(2a), one APIC unreachable (exit code 7 - refused) -> FAIL_O + ( + {topSystem: read_data(dir, "topSystem_3apics_oob.json")}, + "6.0(2a)", + "6.0(3a)", + [0, 7, 0], + script.FAIL_O, + ), + # tversion >= 6.0(2a), all APICs unreachable -> FAIL_O + ( + {topSystem: read_data(dir, "topSystem_3apics_oob.json")}, + "6.0(2a)", + "6.2(3a)", + [28, 28, 28], + script.FAIL_O, + ), + ], +) +def test_logic(run_check, mock_icurl, monkeypatch, cversion, tversion, curl_exit_codes, expected_result): + idx = [0] + + def mock_subprocess_call(cmd, shell=False): + code = curl_exit_codes[idx[0]] if idx[0] < len(curl_exit_codes) else 0 + idx[0] += 1 + return code + + monkeypatch.setattr(script.subprocess, "call", mock_subprocess_call) + result = run_check( + cversion=script.AciVersion(cversion), + tversion=script.AciVersion(tversion) if tversion else None, + ) + assert result.result == expected_result diff --git a/tests/checks/apic_oob_connectivity_check/topSystem_3apics_oob.json b/tests/checks/apic_oob_connectivity_check/topSystem_3apics_oob.json new file mode 100644 index 0000000..38f86d2 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/topSystem_3apics_oob.json @@ -0,0 +1,32 @@ +[ + { + "topSystem": { + "attributes": { + "id": "1", + "role": "controller", + "oobMgmtAddr": "10.30.10.189", + "oobMgmtAddr6": "::" + } + } + }, + { + "topSystem": { + "attributes": { + "id": "2", + "role": "controller", + "oobMgmtAddr": "10.30.10.191", + "oobMgmtAddr6": "::" + } + } + }, + { + "topSystem": { + "attributes": { + "id": "3", + "role": "controller", + "oobMgmtAddr": "10.30.10.193", + "oobMgmtAddr6": "::" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/topSystem_no_oob.json b/tests/checks/apic_oob_connectivity_check/topSystem_no_oob.json new file mode 100644 index 0000000..aa586ef --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/topSystem_no_oob.json @@ -0,0 +1,12 @@ +[ + { + "topSystem": { + "attributes": { + "id": "1", + "role": "controller", + "oobMgmtAddr": "0.0.0.0", + "oobMgmtAddr6": "::" + } + } + } +] From f954a9a0b9767c88018dd2a8d42e6397f9c3d7e0 Mon Sep 17 00:00:00 2001 From: Priyanka Date: Mon, 27 Jul 2026 13:19:30 +0530 Subject: [PATCH 2/5] Added APIC OOB connectivity check --- aci-preupgrade-validation-script.py | 128 ++++++++---------- docs/docs/validations.md | 24 ++-- .../commHttps_invalid_port.json | 1 + ...py => test_apic_oob_connectivity_check.py} | 71 ++++++---- .../test_apic_oob_default_port_check.py | 100 -------------- 5 files changed, 109 insertions(+), 215 deletions(-) create mode 100644 tests/checks/apic_oob_connectivity_check/commHttps_invalid_port.json rename tests/checks/apic_oob_connectivity_check/{test_apic_oob_custom_port_check.py => test_apic_oob_connectivity_check.py} (61%) delete mode 100644 tests/checks/apic_oob_connectivity_check/test_apic_oob_default_port_check.py diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index a57c468..6535aa8 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6702,94 +6702,77 @@ def stale_dbgacEpgSummaryTask_check(tversion, **kwargs): return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def _get_apic_oob_connectivity(topSystems, port): - """Helper to test OOB TCP connectivity to all APIC controllers on a given port. - - Returns: - data (list): rows of [node_id, ip, port, "Unreachable"] for unreachable peers - has_error (bool): True if an unexpected exception occurred during connectivity check - """ - data = [] - has_error = False - - for topSystem in topSystems: - attrs = topSystem['topSystem']['attributes'] - 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: - continue - - try: - if subprocess.call( - 'curl --max-time 5 -k -s -o /dev/null https://{}:{} 2>/dev/null'.format(ip, port), - shell=True - ) in [7, 28]: - data.append([node_id, ip, port, "Unreachable"]) - except Exception as e: - log.error("Exception checking OOB connectivity for node %s: %s", node_id, e) - has_error = True - continue - - return data, has_error - - -@check_wrapper(check_title="APIC OOB Connectivity - Default Port") -def apic_oob_default_port_check(tversion, **kwargs): +@check_wrapper(check_title="APIC OOB Connectivity") +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 TCP port 443 is reachable across the OOB network." - doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#apic-oob-connectivity---default-port' - - if not tversion: - return Result(result=MANUAL, msg=TVER_MISSING) - - if tversion.older_than("6.0(2a)"): - return Result(result=NA, msg=VER_NOT_AFFECTED) + 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' - topSystems = icurl('class', 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")') - if not topSystems: - return Result(result=NA, msg="No APIC controller nodes found.") + def _get_apic_oob_connectivity(topSystems, port): + data = [] + has_error = False - data, has_error = _get_apic_oob_connectivity(topSystems, 443) + for topSystem in topSystems: + attrs = topSystem['topSystem']['attributes'] + node_id = attrs.get('id', '') - if has_error: - result = ERROR - elif data: - result = FAIL_O - return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) + 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: + continue + try: + if subprocess.call( + 'curl --max-time 5 -k -s -o /dev/null https://{}:{} 2>/dev/null'.format(ip, port), + shell=True + ) in [7, 28]: + data.append([node_id, ip, port, "Unreachable"]) + except Exception as e: + log.error("Exception checking OOB connectivity for node %s: %s", node_id, e) + data.append([node_id, ip, port, "Error"]) + has_error = True + continue -@check_wrapper(check_title="APIC OOB Connectivity - Custom HTTPS Port") -def apic_oob_custom_port_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 configured HTTPS port from commHttps is reachable across the OOB network." - doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#apic-oob-connectivity---custom-https-port' + return data, has_error if not tversion: return Result(result=MANUAL, msg=TVER_MISSING) - if cversion.older_than("6.2(1a)") or tversion.older_than("6.2(1a)"): + if tversion.older_than("6.0(2a)"): return Result(result=NA, msg=VER_NOT_AFFECTED) - port = 443 - commHttps = icurl('class', 'commHttps.json') - 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 commHttps HTTPS port from commHttps MO.") - topSystems = icurl('class', 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")') if not topSystems: return Result(result=NA, msg="No APIC controller nodes found.") - data, has_error = _get_apic_oob_connectivity(topSystems, port) + data = [] + has_error = False + + # Default port check: APIC bootx uses port 443 from 6.0(2) + default_data, default_error = _get_apic_oob_connectivity(topSystems, 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(1a)") or tversion.older_than("6.2(1a)")): + port = 443 + commHttps = icurl('class', 'commHttps.json') + 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 commHttps HTTPS port from commHttps MO.") + + if port != 443: # Port 443 already covered by default port check above + custom_data, custom_error = _get_apic_oob_connectivity(topSystems, port) + data.extend(custom_data) + if custom_error: + has_error = True if has_error: result = ERROR @@ -6973,8 +6956,7 @@ class CheckManager: wred_affected_model_check, n9k_c93180yc_fx3_switch_memory_check, stale_dbgacEpgSummaryTask_check, - apic_oob_default_port_check, - apic_oob_custom_port_check, + apic_oob_connectivity_check, ] ssh_checks = [ diff --git a/docs/docs/validations.md b/docs/docs/validations.md index b5f7b9e..aaa2a59 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -206,8 +206,7 @@ Items | Defect | This Script [WRED with Affected FM Models][d35] | CSCwt50713 | :white_check_mark: | :no_entry_sign: [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: -[APIC OOB Connectivity - Default Port][d38] | CSCwu91693 | :white_check_mark: | :white_check_mark: 6.2(2) -[APIC OOB Connectivity - Custom HTTPS Port][d39] | CSCwu91693 | :white_check_mark: | :white_check_mark: 6.2(2) +[APIC OOB Connectivity][d38] | CSCwu91693 | :white_check_mark: | :white_check_mark: 6.2(2) [d1]: #ep-announce-compatibility [d2]: #eventmgr-db-size-defect-susceptibility @@ -246,8 +245,7 @@ Items | Defect | This Script [d35]: #wred-with-affected-fm-models [d36]: #n9k-c93180yc-fx3-switch-memory-less-than-32gb [d37]: #stale-dbgacepgsummarytask-objects -[d38]: #apic-oob-connectivity---default-port -[d39]: #apic-oob-connectivity---custom-https-port +[d38]: #apic-oob-connectivity ## General Check Details @@ -2850,22 +2848,16 @@ Affected versions: 6.1(5e) and below, or 6.2(1g). Contact Cisco TAC for next steps. For more details, refer to the workaround in [CSCwt69100][75]. -### APIC OOB Connectivity - Default Port +### APIC OOB Connectivity -Due to [CSCwu91693][77], when an APIC cluster upgrade is triggered, the orchestrating APIC fans out an HTTPS POST to every peer APIC over the OOB management network on the default port 443 (used by bootx starting from 6.0(2)). If OOB connectivity to any peer APIC is broken at upgrade time, only the reachable APICs receive the trigger and start upgrading. The unreachable APICs are silently skipped, leaving the cluster partially upgraded — a state that cannot be recovered remotely. +Due to [CSCwu91693][77], when an APIC cluster upgrade is triggered, the orchestrating APIC fans out an HTTPS POST to every peer APIC over the OOB management network. If OOB connectivity to any peer APIC is broken at upgrade time, only the reachable APICs receive the trigger and start upgrading. The unreachable APICs are silently skipped, leaving the cluster partially upgraded — a state that cannot be recovered remotely. -This check queries `topSystem` filtered to `role=controller` to collect all APIC OOB management IPs and attempts a TCP connection on port 443 with a 5-second timeout. If any peer is unreachable, the check fails. +This check performs two verifications: -Applicable when target version is 6.0(2) or above. +1. **Default port (443)**: Used by APIC bootx starting from 6.0(2). Applicable when target version is 6.0(2) or above. +2. **Custom HTTPS port (from `commHttps`)**: Used by the upgrade fanout starting from 6.2(1). Applicable when both current and target versions are 6.2(1) or above. If the configured port is the same as the default (443), this step is skipped as it is already covered above. - -### APIC OOB Connectivity - Custom HTTPS Port - -Due to [CSCwu91693][77], the APIC upgrade fanout uses the configured HTTPS port from the `commHttps` policy (default 443) to send the upgrade begin request to peer APICs over the OOB management network. If OOB connectivity on this port is broken for any peer, only a subset of APICs start upgrading, leaving the cluster in a partially upgraded and unrecoverable state. - -This check queries `commHttps` for the configured HTTPS port, queries `topSystem` filtered to `role=controller` for all APIC OOB management IPs, and attempts a TCP connection on the configured port with a 5-second timeout. If any peer is unreachable, the check fails. - -Applicable when both current version and target version are 6.2 or above. +For each applicable check, the script queries `topSystem` filtered to `role=controller` to collect all APIC OOB management IPs and attempts a TCP connection on the relevant port with a 5-second timeout. If any peer is unreachable, the check fails. [0]: https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script diff --git a/tests/checks/apic_oob_connectivity_check/commHttps_invalid_port.json b/tests/checks/apic_oob_connectivity_check/commHttps_invalid_port.json new file mode 100644 index 0000000..ca1c3f2 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/commHttps_invalid_port.json @@ -0,0 +1 @@ +[{"commHttps": {"attributes": {"port": "invalid"}}}] diff --git a/tests/checks/apic_oob_connectivity_check/test_apic_oob_custom_port_check.py b/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py similarity index 61% rename from tests/checks/apic_oob_connectivity_check/test_apic_oob_custom_port_check.py rename to tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py index baa0f36..9457fc5 100644 --- a/tests/checks/apic_oob_connectivity_check/test_apic_oob_custom_port_check.py +++ b/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py @@ -9,7 +9,7 @@ log = logging.getLogger(__name__) dir = os.path.dirname(os.path.abspath(__file__)) -test_function = "apic_oob_custom_port_check" +test_function = "apic_oob_connectivity_check" # icurl queries topSystem = 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")' @@ -22,47 +22,45 @@ # tversion not provided -> MANUAL ( {topSystem: [], commHttps: []}, - "6.2(1a)", + "6.0(2a)", None, [], script.MANUAL, ), - # cversion < 6.2(1a) -> NA (version not affected) + # tversion < 6.0(2a) -> NA (version not affected) ( {topSystem: [], commHttps: []}, - "6.0(3a)", - "6.2(1a)", + "5.2(7f)", + "5.2(7f)", [], script.NA, ), - # tversion < 6.2(1a) -> NA (version not affected) + # tversion >= 6.0(2a), no controller nodes found -> NA ( {topSystem: [], commHttps: []}, - "6.2(1a)", + "6.0(2a)", "6.0(3a)", [], script.NA, ), - # Both >= 6.2, no controller nodes found -> NA + # tversion >= 6.0(2a), all APICs have no OOB configured -> PASS ( - {topSystem: [], commHttps: read_data(dir, "commHttps_default_port.json")}, - "6.2(1a)", - "6.2(2a)", + {topSystem: read_data(dir, "topSystem_no_oob.json"), commHttps: []}, + "6.0(2a)", + "6.0(3a)", [], - script.NA, + script.PASS, ), - # Both >= 6.2, all APICs have no OOB configured -> PASS + # tversion >= 6.0(2a), all APICs reachable on port 443 -> PASS ( - { - topSystem: read_data(dir, "topSystem_no_oob.json"), - commHttps: read_data(dir, "commHttps_default_port.json"), - }, - "6.2(1a)", - "6.2(2a)", - [], + {topSystem: read_data(dir, "topSystem_3apics_oob.json"), commHttps: []}, + "6.0(2a)", + "6.0(3a)", + [0, 0, 0], script.PASS, ), # Both >= 6.2, default port 443, all APICs reachable -> PASS + # (custom port check skipped since commHttps port == 443) ( { topSystem: read_data(dir, "topSystem_3apics_oob.json"), @@ -73,29 +71,39 @@ [0, 0, 0], script.PASS, ), - # Both >= 6.2, default port 443, one APIC unreachable (exit code 28) -> FAIL_UF + # Both >= 6.2, custom port 8443, all APICs reachable on both ports -> PASS + # (default port 443: [0,0,0], custom port 8443: [0,0,0]) ( { topSystem: read_data(dir, "topSystem_3apics_oob.json"), - commHttps: read_data(dir, "commHttps_default_port.json"), + commHttps: read_data(dir, "commHttps_custom_port.json"), }, "6.2(1a)", "6.2(2a)", + [0, 0, 0, 0, 0, 0], + script.PASS, + ), + # tversion >= 6.0(2a), one APIC unreachable on port 443 (exit 28) -> FAIL_UF + ( + {topSystem: read_data(dir, "topSystem_3apics_oob.json"), commHttps: []}, + "6.0(2a)", + "6.0(3a)", [0, 28, 0], script.FAIL_UF, ), - # Both >= 6.2, custom port 8443, one APIC unreachable (exit code 7) -> FAIL_UF + # Both >= 6.2, one APIC unreachable on default port 443 (exit 7) -> FAIL_UF ( { topSystem: read_data(dir, "topSystem_3apics_oob.json"), - commHttps: read_data(dir, "commHttps_custom_port.json"), + commHttps: read_data(dir, "commHttps_default_port.json"), }, "6.2(1a)", "6.2(2a)", [0, 7, 0], script.FAIL_UF, ), - # Both >= 6.2, custom port 8443, all APICs unreachable -> FAIL_UF + # Both >= 6.2, custom port 8443, all unreachable on custom port -> FAIL_UF + # (default port 443 all pass: [0,0,0], custom port 8443 all fail: [28,28,28]) ( { topSystem: read_data(dir, "topSystem_3apics_oob.json"), @@ -103,9 +111,20 @@ }, "6.2(1a)", "6.2(2a)", - [28, 28, 28], + [0, 0, 0, 28, 28, 28], script.FAIL_UF, ), + # Both >= 6.2, commHttps returns invalid port value -> ERROR + ( + { + topSystem: read_data(dir, "topSystem_3apics_oob.json"), + commHttps: read_data(dir, "commHttps_invalid_port.json"), + }, + "6.2(1a)", + "6.2(2a)", + [0, 0, 0], + script.ERROR, + ), ], ) def test_logic(run_check, mock_icurl, monkeypatch, cversion, tversion, curl_exit_codes, expected_result): diff --git a/tests/checks/apic_oob_connectivity_check/test_apic_oob_default_port_check.py b/tests/checks/apic_oob_connectivity_check/test_apic_oob_default_port_check.py deleted file mode 100644 index 5da79e8..0000000 --- a/tests/checks/apic_oob_connectivity_check/test_apic_oob_default_port_check.py +++ /dev/null @@ -1,100 +0,0 @@ -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 = "apic_oob_default_port_check" - -# icurl queries -topSystem = 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")' - - -@pytest.mark.parametrize( - "icurl_outputs, cversion, tversion, curl_exit_codes, expected_result", - [ - # tversion not provided -> MANUAL - ( - {topSystem: []}, - "6.0(2a)", - None, - [], - script.MANUAL, - ), - # tversion < 6.0(2a) -> NA (version not affected) - ( - {topSystem: []}, - "5.2(7f)", - "5.2(7f)", - [], - script.NA, - ), - # tversion >= 6.0(2a), no controller nodes found -> NA - ( - {topSystem: []}, - "6.0(2a)", - "6.0(3a)", - [], - script.NA, - ), - # tversion >= 6.0(2a), all APICs have no OOB configured -> PASS - ( - {topSystem: read_data(dir, "topSystem_no_oob.json")}, - "6.0(2a)", - "6.0(3a)", - [], - script.PASS, - ), - # tversion >= 6.0(2a), all APICs reachable (curl returns 0) -> PASS - ( - {topSystem: read_data(dir, "topSystem_3apics_oob.json")}, - "6.0(2a)", - "6.0(3a)", - [0, 0, 0], - script.PASS, - ), - # tversion >= 6.0(2a), one APIC unreachable (exit code 28 - timeout) -> FAIL_O - ( - {topSystem: read_data(dir, "topSystem_3apics_oob.json")}, - "6.0(2a)", - "6.0(3a)", - [0, 28, 0], - script.FAIL_O, - ), - # tversion >= 6.0(2a), one APIC unreachable (exit code 7 - refused) -> FAIL_O - ( - {topSystem: read_data(dir, "topSystem_3apics_oob.json")}, - "6.0(2a)", - "6.0(3a)", - [0, 7, 0], - script.FAIL_O, - ), - # tversion >= 6.0(2a), all APICs unreachable -> FAIL_O - ( - {topSystem: read_data(dir, "topSystem_3apics_oob.json")}, - "6.0(2a)", - "6.2(3a)", - [28, 28, 28], - script.FAIL_O, - ), - ], -) -def test_logic(run_check, mock_icurl, monkeypatch, cversion, tversion, curl_exit_codes, expected_result): - idx = [0] - - def mock_subprocess_call(cmd, shell=False): - code = curl_exit_codes[idx[0]] if idx[0] < len(curl_exit_codes) else 0 - idx[0] += 1 - return code - - monkeypatch.setattr(script.subprocess, "call", mock_subprocess_call) - result = run_check( - cversion=script.AciVersion(cversion), - tversion=script.AciVersion(tversion) if tversion else None, - ) - assert result.result == expected_result From eeb6f1004a881c073701d5bd70969f5f61b86004 Mon Sep 17 00:00:00 2001 From: Priyanka Date: Tue, 28 Jul 2026 15:28:25 +0530 Subject: [PATCH 3/5] Updated aci-preupgrade-validation-script.py & docs/docs/validations.md --- aci-preupgrade-validation-script.py | 4 ++-- docs/docs/validations.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 6535aa8..1d12bb2 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6728,7 +6728,7 @@ def _get_apic_oob_connectivity(topSystems, port): if subprocess.call( 'curl --max-time 5 -k -s -o /dev/null https://{}:{} 2>/dev/null'.format(ip, port), shell=True - ) in [7, 28]: + ) not in [0, 22]: data.append([node_id, ip, port, "Unreachable"]) except Exception as e: log.error("Exception checking OOB connectivity for node %s: %s", node_id, e) @@ -6758,7 +6758,7 @@ def _get_apic_oob_connectivity(topSystems, port): has_error = True # Custom HTTPS port check: upgrade fanout uses commHttps port from 6.2(1) - if not (cversion.older_than("6.2(1a)") or tversion.older_than("6.2(1a)")): + if not cversion.older_than("6.2(1a)"): port = 443 commHttps = icurl('class', 'commHttps.json') if commHttps: diff --git a/docs/docs/validations.md b/docs/docs/validations.md index aaa2a59..ffe0885 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -2850,12 +2850,12 @@ Contact Cisco TAC for next steps. For more details, refer to the workaround in [ ### APIC OOB Connectivity -Due to [CSCwu91693][77], when an APIC cluster upgrade is triggered, the orchestrating APIC fans out an HTTPS POST to every peer APIC over the OOB management network. If OOB connectivity to any peer APIC is broken at upgrade time, only the reachable APICs receive the trigger and start upgrading. The unreachable APICs are silently skipped, leaving the cluster partially upgraded — a state that cannot be recovered remotely. +APIC bootx uses HTTPS POST for upgrade starting from 6.0(2). Due to [CSCwu91693][77], if OOB connectivity to any peer APIC is broken during upgrade, only the reachable APICs receive the trigger and start upgrading. The unreachable APICs are silently skipped, leaving the cluster partially upgraded. This check performs two verifications: -1. **Default port (443)**: Used by APIC bootx starting from 6.0(2). Applicable when target version is 6.0(2) or above. -2. **Custom HTTPS port (from `commHttps`)**: Used by the upgrade fanout starting from 6.2(1). Applicable when both current and target versions are 6.2(1) or above. If the configured port is the same as the default (443), this step is skipped as it is already covered above. +1. **Default port (443)**: APIC uses bootx starting from 6.0(2). Default HTTPS port used is 443. +2. **Custom HTTPS port (from `commHttps`)**: Starting from 6.2(1), custom ports are supported. Applicable when current version is 6.2(1) or above. For each applicable check, the script queries `topSystem` filtered to `role=controller` to collect all APIC OOB management IPs and attempts a TCP connection on the relevant port with a 5-second timeout. If any peer is unreachable, the check fails. From 730be061f07bb5895a61bb1b19d962e5fbab265c Mon Sep 17 00:00:00 2001 From: Priyanka Date: Tue, 28 Jul 2026 20:23:58 +0530 Subject: [PATCH 4/5] Addressed PR comments --- aci-preupgrade-validation-script.py | 10 +++++----- docs/docs/validations.md | 9 +++------ .../test_apic_oob_connectivity_check.py | 20 +++++++++---------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 1d12bb2..6d1e61c 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6709,7 +6709,7 @@ def apic_oob_connectivity_check(cversion, tversion, **kwargs): 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(topSystems, port): + def get_apic_oob_connectivity(topSystems, port): data = [] has_error = False @@ -6728,7 +6728,7 @@ def _get_apic_oob_connectivity(topSystems, port): if subprocess.call( 'curl --max-time 5 -k -s -o /dev/null https://{}:{} 2>/dev/null'.format(ip, port), shell=True - ) not in [0, 22]: + ) != 0: data.append([node_id, ip, port, "Unreachable"]) except Exception as e: log.error("Exception checking OOB connectivity for node %s: %s", node_id, e) @@ -6752,13 +6752,13 @@ def _get_apic_oob_connectivity(topSystems, port): has_error = False # Default port check: APIC bootx uses port 443 from 6.0(2) - default_data, default_error = _get_apic_oob_connectivity(topSystems, 443) + default_data, default_error = get_apic_oob_connectivity(topSystems, 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(1a)"): + if not cversion.older_than("6.2(1g)"): port = 443 commHttps = icurl('class', 'commHttps.json') if commHttps: @@ -6769,7 +6769,7 @@ def _get_apic_oob_connectivity(topSystems, port): return Result(result=ERROR, msg="Could not read commHttps HTTPS port from commHttps MO.") if port != 443: # Port 443 already covered by default port check above - custom_data, custom_error = _get_apic_oob_connectivity(topSystems, port) + custom_data, custom_error = get_apic_oob_connectivity(topSystems, port) data.extend(custom_data) if custom_error: has_error = True diff --git a/docs/docs/validations.md b/docs/docs/validations.md index ffe0885..45d8cb2 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -2850,14 +2850,11 @@ Contact Cisco TAC for next steps. For more details, refer to the workaround in [ ### APIC OOB Connectivity -APIC bootx uses HTTPS POST for upgrade starting from 6.0(2). Due to [CSCwu91693][77], if OOB connectivity to any peer APIC is broken during upgrade, only the reachable APICs receive the trigger and start upgrading. The unreachable APICs are silently skipped, leaving the cluster partially upgraded. +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][77], 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 performs two verifications: +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. -1. **Default port (443)**: APIC uses bootx starting from 6.0(2). Default HTTPS port used is 443. -2. **Custom HTTPS port (from `commHttps`)**: Starting from 6.2(1), custom ports are supported. Applicable when current version is 6.2(1) or above. - -For each applicable check, the script queries `topSystem` filtered to `role=controller` to collect all APIC OOB management IPs and attempts a TCP connection on the relevant port with a 5-second timeout. If any peer is unreachable, the check fails. +For each applicable port, the script queries `topSystem` filtered to `role=controller` to collect the 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 diff --git a/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py b/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py index 9457fc5..2d1651d 100644 --- a/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py +++ b/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py @@ -59,26 +59,26 @@ [0, 0, 0], script.PASS, ), - # Both >= 6.2, default port 443, all APICs reachable -> PASS + # cversion >= 6.2(1g), default port 443, all APICs reachable -> PASS # (custom port check skipped since commHttps port == 443) ( { topSystem: read_data(dir, "topSystem_3apics_oob.json"), commHttps: read_data(dir, "commHttps_default_port.json"), }, - "6.2(1a)", + "6.2(1g)", "6.2(2a)", [0, 0, 0], script.PASS, ), - # Both >= 6.2, custom port 8443, all APICs reachable on both ports -> PASS + # cversion >= 6.2(1g), custom port 8443, all APICs reachable on both ports -> PASS # (default port 443: [0,0,0], custom port 8443: [0,0,0]) ( { topSystem: read_data(dir, "topSystem_3apics_oob.json"), commHttps: read_data(dir, "commHttps_custom_port.json"), }, - "6.2(1a)", + "6.2(1g)", "6.2(2a)", [0, 0, 0, 0, 0, 0], script.PASS, @@ -91,36 +91,36 @@ [0, 28, 0], script.FAIL_UF, ), - # Both >= 6.2, one APIC unreachable on default port 443 (exit 7) -> FAIL_UF + # cversion >= 6.2(1g), one APIC unreachable on default port 443 (exit 7) -> FAIL_UF ( { topSystem: read_data(dir, "topSystem_3apics_oob.json"), commHttps: read_data(dir, "commHttps_default_port.json"), }, - "6.2(1a)", + "6.2(1g)", "6.2(2a)", [0, 7, 0], script.FAIL_UF, ), - # Both >= 6.2, custom port 8443, all unreachable on custom port -> FAIL_UF + # cversion >= 6.2(1g), custom port 8443, all unreachable on custom port -> FAIL_UF # (default port 443 all pass: [0,0,0], custom port 8443 all fail: [28,28,28]) ( { topSystem: read_data(dir, "topSystem_3apics_oob.json"), commHttps: read_data(dir, "commHttps_custom_port.json"), }, - "6.2(1a)", + "6.2(1g)", "6.2(2a)", [0, 0, 0, 28, 28, 28], script.FAIL_UF, ), - # Both >= 6.2, commHttps returns invalid port value -> ERROR + # cversion >= 6.2(1g), commHttps returns invalid port value -> ERROR ( { topSystem: read_data(dir, "topSystem_3apics_oob.json"), commHttps: read_data(dir, "commHttps_invalid_port.json"), }, - "6.2(1a)", + "6.2(1g)", "6.2(2a)", [0, 0, 0], script.ERROR, From 6f76f21341e1396e5da691c4f5808adbe6ad8368 Mon Sep 17 00:00:00 2001 From: Priyanka Date: Wed, 29 Jul 2026 21:24:52 +0530 Subject: [PATCH 5/5] Addressed PR review comments --- .../test_apic_oob_connectivity_check.py | 16 ++++++++++ .../topSystem_3apics_oob_ipv6.json | 32 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 tests/checks/apic_oob_connectivity_check/topSystem_3apics_oob_ipv6.json diff --git a/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py b/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py index 2d1651d..a233c4e 100644 --- a/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py +++ b/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py @@ -125,6 +125,22 @@ [0, 0, 0], script.ERROR, ), + # IPv6 OOB, all APICs reachable -> PASS + ( + {topSystem: read_data(dir, "topSystem_3apics_oob_ipv6.json"), commHttps: []}, + "6.0(2a)", + "6.0(3a)", + [0, 0, 0], + script.PASS, + ), + # IPv6 OOB, one APIC unreachable (exit 28) -> FAIL_UF + ( + {topSystem: read_data(dir, "topSystem_3apics_oob_ipv6.json"), commHttps: []}, + "6.0(2a)", + "6.0(3a)", + [0, 28, 0], + script.FAIL_UF, + ), ], ) def test_logic(run_check, mock_icurl, monkeypatch, cversion, tversion, curl_exit_codes, expected_result): diff --git a/tests/checks/apic_oob_connectivity_check/topSystem_3apics_oob_ipv6.json b/tests/checks/apic_oob_connectivity_check/topSystem_3apics_oob_ipv6.json new file mode 100644 index 0000000..5c4e8ea --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/topSystem_3apics_oob_ipv6.json @@ -0,0 +1,32 @@ +[ + { + "topSystem": { + "attributes": { + "id": "1", + "role": "controller", + "oobMgmtAddr": "0.0.0.0", + "oobMgmtAddr6": "2001:db8::1" + } + } + }, + { + "topSystem": { + "attributes": { + "id": "2", + "role": "controller", + "oobMgmtAddr": "0.0.0.0", + "oobMgmtAddr6": "2001:db8::2" + } + } + }, + { + "topSystem": { + "attributes": { + "id": "3", + "role": "controller", + "oobMgmtAddr": "0.0.0.0", + "oobMgmtAddr6": "2001:db8::3" + } + } + } +]