diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index f29c66b..c99280b 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -6053,6 +6053,61 @@ def auto_firmware_update_on_switch_check(cversion, tversion, **kwargs): return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) + +@check_wrapper(check_title='Rogue EP Exception List missing on switches') +def rogue_ep_coop_exception_mac_check(cversion, tversion, **kwargs): + result = PASS + headers = ["Rogue Exception MACs Count", "presListener Count"] + data = [] + recommended_action = 'Delete the exception lists and create again before upgrading switches. Or contact Cisco TAC to restore the missing presListener objects.' + recommended_action_pre_apic_upg = 'Change the target version to a fixed version of CSCwp64296.' + doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#rogue-ep-exception-list-missing-on-switches' + + exception_mac_api = 'fvRogueExceptionMac.json?rsp-subtree-include=count' + presListener_api = 'presListener.json?query-target-filter=and(eq(presListener.lstDn,"exceptcont"))&rsp-subtree-include=count' + + # Version ranges + # (unless the patch alphabet is explicitly stated, it means the first version of the train) + # affected source: 5.2(3) <= version < 6.0(3) + # affected target: (6.0(3) <= version < 6.0(9e)) or (6.1(1) <= tversion < 6.1(4)) + + def is_affected_source(ver): + return ver.newer_than("5.2(3a)") and ver.older_than("6.0(3a)") + + def is_affected_target(ver): + in_60 = ver.newer_than("6.0(3a)") and ver.older_than("6.0(9e)") + in_61 = ver.newer_than("6.1(1a)") and ver.older_than("6.1(4h)") + return in_60 or in_61 + + pre_apic_upg = is_affected_source(cversion) and is_affected_target(tversion) # Before APIC upgrade + post_apic_upg = is_affected_target(cversion) and is_affected_target(tversion) and cversion.same_as(tversion) # After APIC upgrade (and before switch) + + if not (pre_apic_upg or post_apic_upg): + return Result(result=NA, msg=VER_NOT_AFFECTED, doc_url=doc_url) + + exception_macs = icurl('class', exception_mac_api) + exception_macs_count = int(exception_macs[0]['moCount']['attributes']['count']) + # Affected versions but no exception MACs. Not susceptible to the issue. + if exception_macs_count == 0: + return Result(result=PASS, doc_url=doc_url) + + # The issue in presListener has yet to happen before APIC upgrade. You can still avoid hitting the issue itself. + if pre_apic_upg: + recommended_action = recommended_action_pre_apic_upg + data.append([exception_macs_count, "N/A"]) + return Result(result=FAIL_O, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) + + # Check presListener entries on APIC after APIC upgrade. + presListener_response = icurl('class', presListener_api) + presListener_count = int(presListener_response[0]['moCount']['attributes']['count']) + if presListener_count >= 0 and presListener_count < 32: + log.info("Insufficient presListener entries ({} found) for {} exception MACs.".format(presListener_count, exception_macs_count)) + result = FAIL_O + data.append([exception_macs_count, "only {} found out of 32".format(presListener_count)]) + + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) + + # ---- Script Execution ---- @@ -6216,7 +6271,7 @@ class CheckManager: isis_database_byte_check, configpush_shard_check, auto_firmware_update_on_switch_check, - + rogue_ep_coop_exception_mac_check, ] ssh_checks = [ # General diff --git a/docs/docs/validations.md b/docs/docs/validations.md index f46e03d..b1b690f 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -194,6 +194,7 @@ Items | Defect | This Script [ISIS DTEPs Byte Size][d27] | CSCwp15375 | :white_check_mark: | :no_entry_sign: [Policydist configpushShardCont Crash][d28] | CSCwp95515 | :white_check_mark: | :no_entry_sign: [Auto Firmware Update on Switch Discovery][d29] | CSCwe83941 | :white_check_mark: | :no_entry_sign: +[Rogue EP Exception List missing on switches][d30] | CSCwp64296 | :white_check_mark: | :no_entry_sign: [d1]: #ep-announce-compatibility [d2]: #eventmgr-db-size-defect-susceptibility @@ -224,6 +225,8 @@ Items | Defect | This Script [d27]: #isis-dteps-byte-size [d28]: #policydist-configpushshardcont-crash [d29]: #auto-firmware-update-on-switch-discovery +[d30]: #rogue-ep-exception-list-missing-on-switches + ## General Check Details @@ -2648,6 +2651,7 @@ 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. + ### Auto Firmware Update on Switch Discovery [Auto Firmware Update on Switch Discovery][63] automatically upgrades a new switch to the target firmware version before registering it to the ACI fabric. This feature activates in three scenarios: @@ -2668,6 +2672,17 @@ To avoid this risk, consider disabling Auto Firmware Update before upgrading to This issue occurs because older switch firmware versions are not compatible with switch images 6.0(3) or newer. The APIC version is not a factor. +### Rogue EP Exception List missing on switches + +The Rogue/COOP Exception List feature, introduced in 5.2(3), allows exclusion of specific MAC addresses from Rogue Endpoint Control and COOP Dampening. Initially, each MAC address had to be configured individually in each bridge domain. In 6.0(3), this feature was enhanced to support fabric-wide exception lists with wildcard options per bridge domain and the ability to exclude MAC addresses in L3Outs. + +However, due to [CSCwp64296][64], when upgrading spine switches to version 6.0(3)+ from an older version with Rogue/COOP Exception Lists configured, some exception lists may not be pushed to the spine switches. As a result, the feature may stop functioning after the upgrade. + +The root cause is that internal objects called `presListener` for Rogue/COOP Exception List, which publish the configuration from APICs to switches, may be missing on the APICs after an upgrade. + +Recommended action: Delete the affected exception list and create it again. If needed, contact Cisco TAC to help recover missing `presListener` objects on APICs. + + [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 @@ -2731,4 +2746,5 @@ To avoid this risk, consider disabling Auto Firmware Update before upgrading to [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/CSCwe83941 -[63]: https://www.cisco.com/c/en/us/td/docs/dcn/aci/apic/all/apic-installation-aci-upgrade-downgrade/Cisco-APIC-Installation-ACI-Upgrade-Downgrade-Guide/m-auto-firmware-update.html \ No newline at end of file +[63]: https://www.cisco.com/c/en/us/td/docs/dcn/aci/apic/all/apic-installation-aci-upgrade-downgrade/Cisco-APIC-Installation-ACI-Upgrade-Downgrade-Guide/m-auto-firmware-update.html +[64]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwp64296 \ No newline at end of file diff --git a/tests/checks/rogue_ep_coop_exception_mac_check/no_rogue_mac_response.json b/tests/checks/rogue_ep_coop_exception_mac_check/no_rogue_mac_response.json new file mode 100644 index 0000000..a2ad913 --- /dev/null +++ b/tests/checks/rogue_ep_coop_exception_mac_check/no_rogue_mac_response.json @@ -0,0 +1,12 @@ +[ + { + "moCount": { + "attributes": { + "childAction": "", + "count": "0", + "dn": "", + "status": "" + } + } + } +] \ No newline at end of file diff --git a/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont.json b/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont.json new file mode 100644 index 0000000..dc8303d --- /dev/null +++ b/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont.json @@ -0,0 +1,12 @@ +[ + { + "moCount": { + "attributes": { + "childAction": "", + "count": "32", + "dn": "", + "status": "" + } + } + } +] \ No newline at end of file diff --git a/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont_31_missing.json b/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont_31_missing.json new file mode 100644 index 0000000..9afbe68 --- /dev/null +++ b/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont_31_missing.json @@ -0,0 +1,12 @@ +[ + { + "moCount": { + "attributes": { + "childAction": "", + "count": "1", + "dn": "", + "status": "" + } + } + } +] \ No newline at end of file diff --git a/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont_32_missing.json b/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont_32_missing.json new file mode 100644 index 0000000..6133f8c --- /dev/null +++ b/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont_32_missing.json @@ -0,0 +1,12 @@ +[ + { + "moCount": { + "attributes": { + "childAction": "", + "count": "0", + "dn": "", + "status": "" + } + } + } +] \ No newline at end of file diff --git a/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont_many_missing.json b/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont_many_missing.json new file mode 100644 index 0000000..cfa27fa --- /dev/null +++ b/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont_many_missing.json @@ -0,0 +1,12 @@ +[ + { + "moCount": { + "attributes": { + "childAction": "", + "count": "27", + "dn": "", + "status": "" + } + } + } +] \ No newline at end of file diff --git a/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont_one_missing.json b/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont_one_missing.json new file mode 100644 index 0000000..6b24f67 --- /dev/null +++ b/tests/checks/rogue_ep_coop_exception_mac_check/presListener_exceptcont_one_missing.json @@ -0,0 +1,12 @@ +[ + { + "moCount": { + "attributes": { + "childAction": "", + "count": "31", + "dn": "", + "status": "" + } + } + } +] \ No newline at end of file diff --git a/tests/checks/rogue_ep_coop_exception_mac_check/rogue_mac_response.json b/tests/checks/rogue_ep_coop_exception_mac_check/rogue_mac_response.json new file mode 100644 index 0000000..0f921b7 --- /dev/null +++ b/tests/checks/rogue_ep_coop_exception_mac_check/rogue_mac_response.json @@ -0,0 +1,12 @@ +[ + { + "moCount": { + "attributes": { + "childAction": "", + "count": "5", + "dn": "", + "status": "" + } + } + } +] \ No newline at end of file diff --git a/tests/checks/rogue_ep_coop_exception_mac_check/test_rogue_ep_coop_exception_mac_check.py b/tests/checks/rogue_ep_coop_exception_mac_check/test_rogue_ep_coop_exception_mac_check.py new file mode 100644 index 0000000..f19dd7d --- /dev/null +++ b/tests/checks/rogue_ep_coop_exception_mac_check/test_rogue_ep_coop_exception_mac_check.py @@ -0,0 +1,202 @@ +import os +import pytest +import logging +import importlib +from helpers.utils import read_data + +log = logging.getLogger(__name__) +dir = os.path.dirname(os.path.abspath(__file__)) + +script = importlib.import_module("aci-preupgrade-validation-script") + +test_function = "rogue_ep_coop_exception_mac_check" + +exception_mac_api = "fvRogueExceptionMac.json?rsp-subtree-include=count" + +presListener_api = "presListener.json" +presListener_api += '?query-target-filter=and(eq(presListener.lstDn,"exceptcont"))&rsp-subtree-include=count' + + +@pytest.mark.parametrize( + "icurl_outputs, tversion, cversion, expected_result, expected_data", + [ + # NA cases (not affected) + # tversion (affected source) + ({}, "5.3(2f)", "5.2(3e)", script.NA, []), # cversion (affected source) + ({}, "6.0(2h)", "5.2(3e)", script.NA, []), # cversion (affected source) + ({}, "5.3(2f)", "5.2(1a)", script.NA, []), # cversion (too old) + ({}, "6.0(2h)", "5.2(1a)", script.NA, []), # cversion (too old) + # tversion (affected target) + ({}, "6.0(3e)", "5.2(1a)", script.NA, []), # cversion (too old) + ({}, "6.1(3g)", "5.2(1a)", script.NA, []), # cversion (too old) + ({}, "6.0(9d)", "6.0(3e)", script.NA, []), # cversion (affected target, but different than tversion)) + ({}, "6.1(3g)", "6.0(3e)", script.NA, []), # cversion (affected target, but different than tversion)) + ({}, "6.1(3g)", "6.1(1f)", script.NA, []), # cversion (affected target, but different than tversion)) + ({}, "6.1(3g)", "6.0(9e)", script.NA, []), # cversion (fixed) + # tversion (fixed) + ({}, "6.0(9e)", "5.2(1a)", script.NA, []), # cversion (too old) + ({}, "6.1(4h)", "5.2(1a)", script.NA, []), # cversion (too old) + ({}, "6.0(9e)", "5.2(3e)", script.NA, []), # cversion (affected source) + ({}, "6.1(4h)", "5.2(3e)", script.NA, []), # cversion (affected source) + ({}, "6.0(9e)", "6.0(3e)", script.NA, []), # cversion (affected target) + ({}, "6.1(4h)", "6.0(3e)", script.NA, []), # cversion (affected target) + ({}, "6.1(4h)", "6.1(1f)", script.NA, []), # cversion (affected target) + ({}, "6.0(9f)", "6.0(9e)", script.NA, []), # cversion (fixed) + ({}, "6.1(4h)", "6.0(9e)", script.NA, []), # cversion (fixed) + ({}, "6.2(1a)", "6.1(4h)", script.NA, []), # cversion (fixed) + # Affected (pre-APIC upgrade) cases + # tversion (affected target), cversion (affected source), no exception MACs + ( + {exception_mac_api: read_data(dir, "no_rogue_mac_response.json")}, + "6.0(3e)", + "5.2(3e)", + script.PASS, + [], + ), + ( + {exception_mac_api: read_data(dir, "no_rogue_mac_response.json")}, + "6.1(3g)", + "5.2(3e)", + script.PASS, + [], + ), + # tversion (affected target), cversion (affected source), exception MACs + ( + {exception_mac_api: read_data(dir, "rogue_mac_response.json")}, + "6.0(3e)", + "5.2(3e)", + script.FAIL_O, + [[5, "N/A"]], + ), + ( + {exception_mac_api: read_data(dir, "rogue_mac_response.json")}, + "6.1(3g)", + "5.2(3e)", + script.FAIL_O, + [[5, "N/A"]], + ), + # Affected (post-APIC upgrade, pre-switch upgrade) cases + # tversion == cversion (affected target), no exception MACs + ( + {exception_mac_api: read_data(dir, "no_rogue_mac_response.json")}, + "6.0(3e)", + "6.0(3e)", + script.PASS, + [], + ), + ( + {exception_mac_api: read_data(dir, "no_rogue_mac_response.json")}, + "6.1(3g)", + "6.1(3g)", + script.PASS, + [], + ), + # tversion == cversion (affected target), exception MACs, presListener entries present + ( + { + exception_mac_api: read_data(dir, "rogue_mac_response.json"), + presListener_api: read_data(dir, "presListener_exceptcont.json"), + }, + "6.0(3e)", + "6.0(3e)", + script.PASS, + [], + ), + ( + { + exception_mac_api: read_data(dir, "rogue_mac_response.json"), + presListener_api: read_data(dir, "presListener_exceptcont.json"), + }, + "6.1(3g)", + "6.1(3g)", + script.PASS, + [], + ), + # tversion == cversion (affected target), exception MACs, presListener entries missing (one, many, 31, or 32(all) missing) + ( + { + exception_mac_api: read_data(dir, "rogue_mac_response.json"), + presListener_api: read_data(dir, "presListener_exceptcont_one_missing.json"), + }, + "6.0(3e)", + "6.0(3e)", + script.FAIL_O, + [[5, "only 31 found out of 32"]], + ), + ( + { + exception_mac_api: read_data(dir, "rogue_mac_response.json"), + presListener_api: read_data(dir, "presListener_exceptcont_one_missing.json"), + }, + "6.1(3g)", + "6.1(3g)", + script.FAIL_O, + [[5, "only 31 found out of 32"]], + ), + ( + { + exception_mac_api: read_data(dir, "rogue_mac_response.json"), + presListener_api: read_data(dir, "presListener_exceptcont_many_missing.json"), + }, + "6.0(3e)", + "6.0(3e)", + script.FAIL_O, + [[5, "only 27 found out of 32"]], + ), + ( + { + exception_mac_api: read_data(dir, "rogue_mac_response.json"), + presListener_api: read_data(dir, "presListener_exceptcont_many_missing.json"), + }, + "6.1(3g)", + "6.1(3g)", + script.FAIL_O, + [[5, "only 27 found out of 32"]], + ), + ( + { + exception_mac_api: read_data(dir, "rogue_mac_response.json"), + presListener_api: read_data(dir, "presListener_exceptcont_31_missing.json"), + }, + "6.0(3e)", + "6.0(3e)", + script.FAIL_O, + [[5, "only 1 found out of 32"]], + ), + ( + { + exception_mac_api: read_data(dir, "rogue_mac_response.json"), + presListener_api: read_data(dir, "presListener_exceptcont_31_missing.json"), + }, + "6.1(3g)", + "6.1(3g)", + script.FAIL_O, + [[5, "only 1 found out of 32"]], + ), + ( + { + exception_mac_api: read_data(dir, "rogue_mac_response.json"), + presListener_api: read_data(dir, "presListener_exceptcont_32_missing.json"), + }, + "6.0(3e)", + "6.0(3e)", + script.FAIL_O, + [[5, "only 0 found out of 32"]], + ), + ( + { + exception_mac_api: read_data(dir, "rogue_mac_response.json"), + presListener_api: read_data(dir, "presListener_exceptcont_32_missing.json"), + }, + "6.1(3g)", + "6.1(3g)", + script.FAIL_O, + [[5, "only 0 found out of 32"]], + ), + ], +) +def test_rogue_ep_coop_exception_mac_check(run_check, mock_icurl, tversion, cversion, expected_result, expected_data): + """Test rogue_ep_coop_exception_mac_check with various scenarios.""" + result = run_check(cversion=script.AciVersion(cversion), tversion=script.AciVersion(tversion) if tversion else None) + assert result.result == expected_result + assert result.data == expected_data