From d5849189584e2790f01553dcd53f5d41df6de372 Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Mon, 7 Sep 2026 09:46:44 +0530 Subject: [PATCH 01/21] dynamic bootflash utilization --- .DS_Store | Bin 0 -> 6148 bytes aci-preupgrade-validation-script.py | 79 ++++++++++++------ .../fabricNode_1apic.json | 10 +++ .../fabricNode_3apics.json | 26 ++++++ .../fabricPodBlk_none.json | 1 + .../fabricPodBlk_pod1.json | 11 +++ .../fabricPodS_default_all.json | 10 +++ .../fabricPodS_default_and_pod1_range.json | 18 ++++ .../fabricPodS_two_all.json | 18 ++++ .../fabricRsCommPol_default_and_custom.json | 18 ++++ .../fabricRsCommPol_none.json | 1 + .../fabricRsCommPol_to_custom.json | 10 +++ .../fabricRsCommPol_to_default.json | 10 +++ .../fabricRsPodPGrp_default.json | 10 +++ .../fabricRsPodPGrp_default_and_pod1.json | 18 ++++ .../fabricRsPodPGrp_two_all.json | 18 ++++ .../topSystem_mixed_malformed.json | 25 ++++++ .../topSystem_oob_link_local_only.json | 12 +++ .../topSystem_partial_2of3.json | 22 +++++ ...mwareFirmware_dual_image_insufficient.json | 35 ++++++++ ...irmwareFirmware_dual_image_sufficient.json | 35 ++++++++ .../maintUpgJob_not_downloaded.json | 1 - .../maintUpgJob_old_ver_no_prop.json | 10 --- .../maintUpgJob_pre_downloaded.json | 16 ---- .../test_switch_bootflash_usage_check.py | 58 ++++++++++--- 25 files changed, 408 insertions(+), 64 deletions(-) create mode 100644 .DS_Store create mode 100644 tests/checks/apic_oob_connectivity_check/fabricNode_1apic.json create mode 100644 tests/checks/apic_oob_connectivity_check/fabricNode_3apics.json create mode 100644 tests/checks/apic_oob_connectivity_check/fabricPodBlk_none.json create mode 100644 tests/checks/apic_oob_connectivity_check/fabricPodBlk_pod1.json create mode 100644 tests/checks/apic_oob_connectivity_check/fabricPodS_default_all.json create mode 100644 tests/checks/apic_oob_connectivity_check/fabricPodS_default_and_pod1_range.json create mode 100644 tests/checks/apic_oob_connectivity_check/fabricPodS_two_all.json create mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsCommPol_default_and_custom.json create mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsCommPol_none.json create mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsCommPol_to_custom.json create mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsCommPol_to_default.json create mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_default.json create mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_default_and_pod1.json create mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_two_all.json create mode 100644 tests/checks/apic_oob_connectivity_check/topSystem_mixed_malformed.json create mode 100644 tests/checks/apic_oob_connectivity_check/topSystem_oob_link_local_only.json create mode 100644 tests/checks/apic_oob_connectivity_check/topSystem_partial_2of3.json create mode 100644 tests/checks/switch_bootflash_usage_check/firmwareFirmware_dual_image_insufficient.json create mode 100644 tests/checks/switch_bootflash_usage_check/firmwareFirmware_dual_image_sufficient.json delete mode 100644 tests/checks/switch_bootflash_usage_check/maintUpgJob_not_downloaded.json delete mode 100644 tests/checks/switch_bootflash_usage_check/maintUpgJob_old_ver_no_prop.json delete mode 100644 tests/checks/switch_bootflash_usage_check/maintUpgJob_pre_downloaded.json diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0= 50) and (node not in predownloaded_nodes): - data.append([pod, node, usage]) + if avail < required_space_kb: + data.append([pod, node, round(avail / 1024.0, 2), round(required_space_kb / 1024.0, 2)]) if not data: result = PASS - msg = 'All below 50% or pre-downloaded' + msg = 'All nodes have sufficient bootflash space' return Result(result=result, msg=msg, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) diff --git a/tests/checks/apic_oob_connectivity_check/fabricNode_1apic.json b/tests/checks/apic_oob_connectivity_check/fabricNode_1apic.json new file mode 100644 index 00000000..d7b5f8ef --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricNode_1apic.json @@ -0,0 +1,10 @@ +[ + { + "fabricNode": { + "attributes": { + "id": "1", + "role": "controller" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/fabricNode_3apics.json b/tests/checks/apic_oob_connectivity_check/fabricNode_3apics.json new file mode 100644 index 00000000..0a7960a8 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricNode_3apics.json @@ -0,0 +1,26 @@ +[ + { + "fabricNode": { + "attributes": { + "id": "1", + "role": "controller" + } + } + }, + { + "fabricNode": { + "attributes": { + "id": "2", + "role": "controller" + } + } + }, + { + "fabricNode": { + "attributes": { + "id": "3", + "role": "controller" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/fabricPodBlk_none.json b/tests/checks/apic_oob_connectivity_check/fabricPodBlk_none.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricPodBlk_none.json @@ -0,0 +1 @@ +[] diff --git a/tests/checks/apic_oob_connectivity_check/fabricPodBlk_pod1.json b/tests/checks/apic_oob_connectivity_check/fabricPodBlk_pod1.json new file mode 100644 index 00000000..6472b002 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricPodBlk_pod1.json @@ -0,0 +1,11 @@ +[ + { + "fabricPodBlk": { + "attributes": { + "dn": "uni/fabric/podprofile-default/pods-pod1-typ-range/podblk-blk1", + "from": "1", + "to": "1" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/fabricPodS_default_all.json b/tests/checks/apic_oob_connectivity_check/fabricPodS_default_all.json new file mode 100644 index 00000000..da78533a --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricPodS_default_all.json @@ -0,0 +1,10 @@ +[ + { + "fabricPodS": { + "attributes": { + "dn": "uni/fabric/podprofile-default/pods-default-typ-ALL", + "type": "ALL" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/fabricPodS_default_and_pod1_range.json b/tests/checks/apic_oob_connectivity_check/fabricPodS_default_and_pod1_range.json new file mode 100644 index 00000000..abd565a0 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricPodS_default_and_pod1_range.json @@ -0,0 +1,18 @@ +[ + { + "fabricPodS": { + "attributes": { + "dn": "uni/fabric/podprofile-default/pods-default-typ-ALL", + "type": "ALL" + } + } + }, + { + "fabricPodS": { + "attributes": { + "dn": "uni/fabric/podprofile-default/pods-pod1-typ-range", + "type": "range" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/fabricPodS_two_all.json b/tests/checks/apic_oob_connectivity_check/fabricPodS_two_all.json new file mode 100644 index 00000000..d816f13d --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricPodS_two_all.json @@ -0,0 +1,18 @@ +[ + { + "fabricPodS": { + "attributes": { + "dn": "uni/fabric/podprofile-default/pods-default-typ-ALL", + "type": "ALL" + } + } + }, + { + "fabricPodS": { + "attributes": { + "dn": "uni/fabric/podprofile-other/pods-other-typ-ALL", + "type": "ALL" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/fabricRsCommPol_default_and_custom.json b/tests/checks/apic_oob_connectivity_check/fabricRsCommPol_default_and_custom.json new file mode 100644 index 00000000..93c6da15 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricRsCommPol_default_and_custom.json @@ -0,0 +1,18 @@ +[ + { + "fabricRsCommPol": { + "attributes": { + "dn": "uni/fabric/funcprof/podpgrp-default/rscommPol", + "tDn": "uni/fabric/comm-default" + } + } + }, + { + "fabricRsCommPol": { + "attributes": { + "dn": "uni/fabric/funcprof/podpgrp-pod1/rscommPol", + "tDn": "uni/fabric/comm-custom-port" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/fabricRsCommPol_none.json b/tests/checks/apic_oob_connectivity_check/fabricRsCommPol_none.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricRsCommPol_none.json @@ -0,0 +1 @@ +[] diff --git a/tests/checks/apic_oob_connectivity_check/fabricRsCommPol_to_custom.json b/tests/checks/apic_oob_connectivity_check/fabricRsCommPol_to_custom.json new file mode 100644 index 00000000..1f193dea --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricRsCommPol_to_custom.json @@ -0,0 +1,10 @@ +[ + { + "fabricRsCommPol": { + "attributes": { + "dn": "uni/fabric/funcprof/podpgrp-default/rscommPol", + "tDn": "uni/fabric/comm-custom-port" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/fabricRsCommPol_to_default.json b/tests/checks/apic_oob_connectivity_check/fabricRsCommPol_to_default.json new file mode 100644 index 00000000..4998d07d --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricRsCommPol_to_default.json @@ -0,0 +1,10 @@ +[ + { + "fabricRsCommPol": { + "attributes": { + "dn": "uni/fabric/funcprof/podpgrp-default/rscommPol", + "tDn": "uni/fabric/comm-default" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_default.json b/tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_default.json new file mode 100644 index 00000000..87d6b9e3 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_default.json @@ -0,0 +1,10 @@ +[ + { + "fabricRsPodPGrp": { + "attributes": { + "dn": "uni/fabric/podprofile-default/pods-default-typ-ALL/rspodPGrp", + "tDn": "uni/fabric/funcprof/podpgrp-default" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_default_and_pod1.json b/tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_default_and_pod1.json new file mode 100644 index 00000000..492b2c7d --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_default_and_pod1.json @@ -0,0 +1,18 @@ +[ + { + "fabricRsPodPGrp": { + "attributes": { + "dn": "uni/fabric/podprofile-default/pods-default-typ-ALL/rspodPGrp", + "tDn": "uni/fabric/funcprof/podpgrp-default" + } + } + }, + { + "fabricRsPodPGrp": { + "attributes": { + "dn": "uni/fabric/podprofile-default/pods-pod1-typ-range/rspodPGrp", + "tDn": "uni/fabric/funcprof/podpgrp-pod1" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_two_all.json b/tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_two_all.json new file mode 100644 index 00000000..bc24d585 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_two_all.json @@ -0,0 +1,18 @@ +[ + { + "fabricRsPodPGrp": { + "attributes": { + "dn": "uni/fabric/podprofile-default/pods-default-typ-ALL/rspodPGrp", + "tDn": "uni/fabric/funcprof/podpgrp-default" + } + } + }, + { + "fabricRsPodPGrp": { + "attributes": { + "dn": "uni/fabric/podprofile-other/pods-other-typ-ALL/rspodPGrp", + "tDn": "uni/fabric/funcprof/podpgrp-other" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/topSystem_mixed_malformed.json b/tests/checks/apic_oob_connectivity_check/topSystem_mixed_malformed.json new file mode 100644 index 00000000..9cef2ab5 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/topSystem_mixed_malformed.json @@ -0,0 +1,25 @@ +[ + { + "topSystem": { + "attributes": { + "id": "1", + "role": "controller", + "oobMgmtAddr": "10.30.10.189", + "oobMgmtAddr6": "::" + } + } + }, + { + "topSystem": {} + }, + { + "topSystem": { + "attributes": { + "id": "3", + "role": "controller", + "oobMgmtAddr": "10.30.10.193", + "oobMgmtAddr6": "::" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/topSystem_oob_link_local_only.json b/tests/checks/apic_oob_connectivity_check/topSystem_oob_link_local_only.json new file mode 100644 index 00000000..cdfc5eb6 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/topSystem_oob_link_local_only.json @@ -0,0 +1,12 @@ +[ + { + "topSystem": { + "attributes": { + "id": "1", + "role": "controller", + "oobMgmtAddr": "0.0.0.0", + "oobMgmtAddr6": "fe80::1" + } + } + } +] diff --git a/tests/checks/apic_oob_connectivity_check/topSystem_partial_2of3.json b/tests/checks/apic_oob_connectivity_check/topSystem_partial_2of3.json new file mode 100644 index 00000000..9aaa2a15 --- /dev/null +++ b/tests/checks/apic_oob_connectivity_check/topSystem_partial_2of3.json @@ -0,0 +1,22 @@ +[ + { + "topSystem": { + "attributes": { + "id": "1", + "role": "controller", + "oobMgmtAddr": "10.30.10.189", + "oobMgmtAddr6": "::" + } + } + }, + { + "topSystem": { + "attributes": { + "id": "3", + "role": "controller", + "oobMgmtAddr": "10.30.10.193", + "oobMgmtAddr6": "::" + } + } + } +] diff --git a/tests/checks/switch_bootflash_usage_check/firmwareFirmware_dual_image_insufficient.json b/tests/checks/switch_bootflash_usage_check/firmwareFirmware_dual_image_insufficient.json new file mode 100644 index 00000000..0200064e --- /dev/null +++ b/tests/checks/switch_bootflash_usage_check/firmwareFirmware_dual_image_insufficient.json @@ -0,0 +1,35 @@ +[ + { + "firmwareFirmware": { + "attributes": { + "dn": "fwrepo/fw-aci-n9000-system.16.1.5e.bin", + "fullVersion": "n9000-16.1(5e)", + "isoname": "aci-n9000-dk9.16.1.5e.bin", + "name": "aci-n9000-system.16.1.5e.bin", + "size": "3000000000" + } + } + }, + { + "firmwareFirmware": { + "attributes": { + "dn": "fwrepo/fw-aci-n9000-system.16.1.5e-cs_64.bin", + "fullVersion": "n9000-16.1(5e)", + "isoname": "aci-n9000-dk9.16.1.5e-cs_64.bin", + "name": "aci-n9000-system.16.1.5e-cs_64.bin", + "size": "3000000000" + } + } + }, + { + "firmwareFirmware": { + "attributes": { + "dn": "fwrepo/fw-aci-n9000-system.15.2.8h.bin", + "fullVersion": "n9000-15.2(8h)", + "isoname": "aci-n9000-dk9.15.2.8h.bin", + "name": "aci-n9000-system.15.2.8h.bin", + "size": "2000000000" + } + } + } +] diff --git a/tests/checks/switch_bootflash_usage_check/firmwareFirmware_dual_image_sufficient.json b/tests/checks/switch_bootflash_usage_check/firmwareFirmware_dual_image_sufficient.json new file mode 100644 index 00000000..652f5768 --- /dev/null +++ b/tests/checks/switch_bootflash_usage_check/firmwareFirmware_dual_image_sufficient.json @@ -0,0 +1,35 @@ +[ + { + "firmwareFirmware": { + "attributes": { + "dn": "fwrepo/fw-aci-n9000-system.16.1.5e.bin", + "fullVersion": "n9000-16.1(5e)", + "isoname": "aci-n9000-dk9.16.1.5e.bin", + "name": "aci-n9000-system.16.1.5e.bin", + "size": "1500000000" + } + } + }, + { + "firmwareFirmware": { + "attributes": { + "dn": "fwrepo/fw-aci-n9000-system.16.1.5e-cs_64.bin", + "fullVersion": "n9000-16.1(5e)", + "isoname": "aci-n9000-dk9.16.1.5e-cs_64.bin", + "name": "aci-n9000-system.16.1.5e-cs_64.bin", + "size": "1500000000" + } + } + }, + { + "firmwareFirmware": { + "attributes": { + "dn": "fwrepo/fw-aci-n9000-system.15.2.8h.bin", + "fullVersion": "n9000-15.2(8h)", + "isoname": "aci-n9000-dk9.15.2.8h.bin", + "name": "aci-n9000-system.15.2.8h.bin", + "size": "2000000000" + } + } + } +] diff --git a/tests/checks/switch_bootflash_usage_check/maintUpgJob_not_downloaded.json b/tests/checks/switch_bootflash_usage_check/maintUpgJob_not_downloaded.json deleted file mode 100644 index 0637a088..00000000 --- a/tests/checks/switch_bootflash_usage_check/maintUpgJob_not_downloaded.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/tests/checks/switch_bootflash_usage_check/maintUpgJob_old_ver_no_prop.json b/tests/checks/switch_bootflash_usage_check/maintUpgJob_old_ver_no_prop.json deleted file mode 100644 index 9f74f58d..00000000 --- a/tests/checks/switch_bootflash_usage_check/maintUpgJob_old_ver_no_prop.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "error": { - "attributes": { - "code": "121", - "text": "Prop 'dnldStatus' not found in class 'maintUpgJob' property table" - } - } - } -] \ No newline at end of file diff --git a/tests/checks/switch_bootflash_usage_check/maintUpgJob_pre_downloaded.json b/tests/checks/switch_bootflash_usage_check/maintUpgJob_pre_downloaded.json deleted file mode 100644 index efc36e70..00000000 --- a/tests/checks/switch_bootflash_usage_check/maintUpgJob_pre_downloaded.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "maintUpgJob": { - "attributes": { - "desiredVersion": "n9000-16.0(2h)", - "dn": "topology/pod-1/node-101/sys/fwstatuscont/upgjob", - "dnldPercent": "100", - "dnldStatus": "downloaded", - "startDate": "2023-11-16T10:15:22.894-08:00", - "status": "", - "upgradeStatus": "scheduled", - "upgradeStatusStr": "Scheduled" - } - } - } -] \ No newline at end of file diff --git a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py index 18e9c1c3..1082197c 100644 --- a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py +++ b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py @@ -15,48 +15,82 @@ partitions = "eqptcapacityFSPartition.json" partitions += '?query-target-filter=eq(eqptcapacityFSPartition.path,"/bootflash")' -download_sts = "maintUpgJob.json" -download_sts += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded")' -download_sts += ',eq(maintUpgJob.desiredVersion,"n9000-16.0(2h)"))' +firmware = 'firmwareFirmware.json?query-target-filter=eq(firmwareFirmware.type,"switch")' + +# Firmware images for target 6.0(2h): both current/target are >= 6.0(2a) so both +# 32/64-bit isos are considered. The 64-bit image is the larger of the two. +firmware_dual_602 = [ + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.0.2h.bin", "size": "2000000000"}}}, + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.0.2h-cs_64.bin", "size": "3000000000"}}}, +] @pytest.mark.parametrize( - "icurl_outputs, tversion, expected_result", + "icurl_outputs, cversion, tversion, expected_result", [ ( { partitions: [], - download_sts: [], }, + "6.0(3a)", "6.0(2h)", script.MANUAL, ), ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: read_data(dir, "maintUpgJob_not_downloaded.json"), + firmware: firmware_dual_602, }, + "6.0(3a)", "6.0(2h)", script.FAIL_UF, ), + # Both current and target are pre-6.0(2a): only the single 32-bit iso matters. ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: read_data(dir, "maintUpgJob_pre_downloaded.json"), + firmware: [ + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.15.2.8h.bin", "size": "1000000000"}}}, + ], }, - "6.0(2h)", + "5.2(8h)", + "5.2(8h)", script.PASS, ), + # Crossing 6.0(2a): current 32-bit image size is deducted from the two target isos. ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: read_data(dir, "maintUpgJob_old_ver_no_prop.json"), + firmware: read_data(dir, "firmwareFirmware_dual_image_insufficient.json"), }, - "6.0(2h)", + "5.2(8h)", + "6.1(5e)", script.FAIL_UF, ), + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + firmware: read_data(dir, "firmwareFirmware_dual_image_sufficient.json"), + }, + "5.2(8h)", + "6.1(5e)", + script.PASS, + ), + # Target image not yet uploaded to the Firmware Repository. + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + firmware: [], + }, + "6.0(3a)", + "6.0(2h)", + script.MANUAL, + ), ], ) -def test_logic(run_check, mock_icurl, tversion, expected_result): - result = run_check(tversion=script.AciVersion(tversion)) +def test_logic(run_check, mock_icurl, cversion, tversion, expected_result): + result = run_check( + cversion=script.AciVersion(cversion), + tversion=script.AciVersion(tversion), + ) assert result.result == expected_result From de146e5ca25887a879b229b68fb103480e4651f4 Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Mon, 7 Sep 2026 11:10:25 +0530 Subject: [PATCH 02/21] dynamic bootflash utilization --- aci-preupgrade-validation-script.py | 18 ++++ .../test_switch_bootflash_usage_check.py | 101 +++++++++++++++++- 2 files changed, 117 insertions(+), 2 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 1a97ebe6..92edcfd4 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2177,6 +2177,22 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): if not partitions: return Result(result=MANUAL, msg='/bootflash directory not found. Check switch health.', doc_url=doc_url) + download_sts_api = 'maintUpgJob.json' + download_sts_api += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100"))' + download_sts_api += '&rsp-subtree=full' + + try: + download_sts = icurl('class', download_sts_api) + except OldVerPropNotFound: + # Older versions don't have 'dnldStatus'/'dnldPercent' params + download_sts = [] + + predownloaded_nodes = {} + for maintUpgJob in download_sts: + dn = re.search(node_regex, maintUpgJob['maintUpgJob']['attributes']['dn']) + if dn: + predownloaded_nodes[dn.group("node")] = maintUpgJob['maintUpgJob']['attributes'] + # Starting 6.0(2a), switch images are shipped as separate 32-bit and 64-bit # isos (`-cs_64` suffix for 64-bit). Below that, only a single 32-bit iso exists. boundary_version = "6.0(2a)" @@ -2224,6 +2240,8 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): dn = re.search(node_regex, eqptcapacityFSPartition['eqptcapacityFSPartition']['attributes']['dn']) pod = dn.group("pod") node = dn.group("node") + if node in predownloaded_nodes: + continue avail = int(eqptcapacityFSPartition['eqptcapacityFSPartition']['attributes']['avail']) if avail < required_space_kb: diff --git a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py index 1082197c..24ae3d04 100644 --- a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py +++ b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py @@ -17,17 +17,62 @@ firmware = 'firmwareFirmware.json?query-target-filter=eq(firmwareFirmware.type,"switch")' +download_sts = 'maintUpgJob.json' +download_sts += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100"))' +download_sts += '&rsp-subtree=full' + +# No pre-downloaded nodes unless a test overrides this key. +no_predownload = [] + +# Older versions don't have `dnldStatus`/`dnldPercent` props on `maintUpgJob`. +old_ver_no_prop = [{"error": {"attributes": {"code": "400", "text": "Prop 'dnldStatus' not found in class 'maintUpgJob' property table"}}}] + # Firmware images for target 6.0(2h): both current/target are >= 6.0(2a) so both # 32/64-bit isos are considered. The 64-bit image is the larger of the two. +# Of all nodes in eqptcapacityFSPartition.json, only node-101 (avail 5347648 KB) +# falls below the resulting required space (~5859375 KB) and thus fails. firmware_dual_602 = [ {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.0.2h.bin", "size": "2000000000"}}}, {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.0.2h-cs_64.bin", "size": "3000000000"}}}, ] +# node-101 has fully downloaded/extracted the target image already. +maintUpgJob_node_101_downloaded = [ + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-101/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, +] + +# node-999 (not present in eqptcapacityFSPartition.json) has pre-downloaded the image. +maintUpgJob_node_999_downloaded = [ + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-999/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, +] + +# Every node in eqptcapacityFSPartition.json has fully pre-downloaded the target image. +maintUpgJob_all_downloaded = [ + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-102/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-103/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-205/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-206/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-1002/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-1001/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-2002/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-2003/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-2001/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-2010/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-101/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, +] + @pytest.mark.parametrize( "icurl_outputs, cversion, tversion, expected_result", [ + # No tversion provided. + ( + {}, + None, + None, + script.MANUAL, + ), + # /bootflash partition objects not found at all. Returns before maintUpgJob is queried. ( { partitions: [], @@ -36,9 +81,11 @@ "6.0(2h)", script.MANUAL, ), + # Baseline failure: node-101 lacks sufficient space and is not pre-downloaded. ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts: no_predownload, firmware: firmware_dual_602, }, "6.0(3a)", @@ -49,6 +96,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts: no_predownload, firmware: [ {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.15.2.8h.bin", "size": "1000000000"}}}, ], @@ -61,6 +109,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts: no_predownload, firmware: read_data(dir, "firmwareFirmware_dual_image_insufficient.json"), }, "5.2(8h)", @@ -70,6 +119,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts: no_predownload, firmware: read_data(dir, "firmwareFirmware_dual_image_sufficient.json"), }, "5.2(8h)", @@ -80,17 +130,64 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts: no_predownload, firmware: [], }, "6.0(3a)", "6.0(2h)", script.MANUAL, ), + # node-101 (the only node that would otherwise fail) already fully downloaded + # the target image, so it's excluded from the check and the result is PASS. + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts: maintUpgJob_node_101_downloaded, + firmware: firmware_dual_602, + }, + "6.0(3a)", + "6.0(2h)", + script.PASS, + ), + # A pre-downloaded node unrelated to the failing node doesn't change the outcome. + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts: maintUpgJob_node_999_downloaded, + firmware: firmware_dual_602, + }, + "6.0(3a)", + "6.0(2h)", + script.FAIL_UF, + ), + # Every node has pre-downloaded the image, so all are skipped and the result is PASS. + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts: maintUpgJob_all_downloaded, + firmware: firmware_dual_602, + }, + "6.0(3a)", + "6.0(2h)", + script.PASS, + ), + # Older versions don't have `dnldStatus`/`dnldPercent` on `maintUpgJob`: `OldVerPropNotFound` + # is caught and treated as no pre-downloaded nodes found (node-101 still fails). + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts: old_ver_no_prop, + firmware: firmware_dual_602, + }, + "6.0(3a)", + "6.0(2h)", + script.FAIL_UF, + ), ], ) def test_logic(run_check, mock_icurl, cversion, tversion, expected_result): result = run_check( - cversion=script.AciVersion(cversion), - tversion=script.AciVersion(tversion), + cversion=script.AciVersion(cversion) if cversion else None, + tversion=script.AciVersion(tversion) if tversion else None, ) assert result.result == expected_result From 330af6ec2cd54ba521e2acf18a3ee85674b6b7af Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Mon, 7 Sep 2026 11:26:08 +0530 Subject: [PATCH 03/21] dynamic bootflash utilization --- .../firmwareFirmware_dual_602.json | 4 + .../maintUpgJob_all_downloaded.json | 13 +++ .../maintUpgJob_all_of_failing.json | 10 +++ .../maintUpgJob_old_ver_no_prop.json | 10 +++ .../maintUpgJob_partial_missing_empty.json | 5 ++ .../test_switch_bootflash_usage_check.py | 85 +++++++++++++++---- 6 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 tests/checks/switch_bootflash_usage_check/firmwareFirmware_dual_602.json create mode 100644 tests/checks/switch_bootflash_usage_check/maintUpgJob_all_downloaded.json create mode 100644 tests/checks/switch_bootflash_usage_check/maintUpgJob_all_of_failing.json create mode 100644 tests/checks/switch_bootflash_usage_check/maintUpgJob_old_ver_no_prop.json create mode 100644 tests/checks/switch_bootflash_usage_check/maintUpgJob_partial_missing_empty.json diff --git a/tests/checks/switch_bootflash_usage_check/firmwareFirmware_dual_602.json b/tests/checks/switch_bootflash_usage_check/firmwareFirmware_dual_602.json new file mode 100644 index 00000000..4c12c879 --- /dev/null +++ b/tests/checks/switch_bootflash_usage_check/firmwareFirmware_dual_602.json @@ -0,0 +1,4 @@ +[ + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.0.2h.bin", "size": "2000000000"}}}, + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.0.2h-cs_64.bin", "size": "3000000000"}}} +] diff --git a/tests/checks/switch_bootflash_usage_check/maintUpgJob_all_downloaded.json b/tests/checks/switch_bootflash_usage_check/maintUpgJob_all_downloaded.json new file mode 100644 index 00000000..207a2544 --- /dev/null +++ b/tests/checks/switch_bootflash_usage_check/maintUpgJob_all_downloaded.json @@ -0,0 +1,13 @@ +[ + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-102/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-103/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-205/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-206/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-1002/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-1001/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-2002/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-2003/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-2001/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-2010/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-101/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}} +] diff --git a/tests/checks/switch_bootflash_usage_check/maintUpgJob_all_of_failing.json b/tests/checks/switch_bootflash_usage_check/maintUpgJob_all_of_failing.json new file mode 100644 index 00000000..f0ca5167 --- /dev/null +++ b/tests/checks/switch_bootflash_usage_check/maintUpgJob_all_of_failing.json @@ -0,0 +1,10 @@ +[ + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-102/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-103/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-205/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-206/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-1002/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-1001/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-2001/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-101/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}} +] diff --git a/tests/checks/switch_bootflash_usage_check/maintUpgJob_old_ver_no_prop.json b/tests/checks/switch_bootflash_usage_check/maintUpgJob_old_ver_no_prop.json new file mode 100644 index 00000000..d9f93d7e --- /dev/null +++ b/tests/checks/switch_bootflash_usage_check/maintUpgJob_old_ver_no_prop.json @@ -0,0 +1,10 @@ +[ + { + "error": { + "attributes": { + "code": "400", + "text": "Prop 'dnldStatus' not found in class 'maintUpgJob' property table" + } + } + } +] diff --git a/tests/checks/switch_bootflash_usage_check/maintUpgJob_partial_missing_empty.json b/tests/checks/switch_bootflash_usage_check/maintUpgJob_partial_missing_empty.json new file mode 100644 index 00000000..552d314d --- /dev/null +++ b/tests/checks/switch_bootflash_usage_check/maintUpgJob_partial_missing_empty.json @@ -0,0 +1,5 @@ +[ + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-102/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-103/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, + {"maintUpgJob": {"attributes": {"dn": "", "dnldStatus": "downloaded", "dnldPercent": "100"}}} +] diff --git a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py index 24ae3d04..e58df54a 100644 --- a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py +++ b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py @@ -25,16 +25,13 @@ no_predownload = [] # Older versions don't have `dnldStatus`/`dnldPercent` props on `maintUpgJob`. -old_ver_no_prop = [{"error": {"attributes": {"code": "400", "text": "Prop 'dnldStatus' not found in class 'maintUpgJob' property table"}}}] +old_ver_no_prop = read_data(dir, "maintUpgJob_old_ver_no_prop.json") # Firmware images for target 6.0(2h): both current/target are >= 6.0(2a) so both # 32/64-bit isos are considered. The 64-bit image is the larger of the two. # Of all nodes in eqptcapacityFSPartition.json, only node-101 (avail 5347648 KB) # falls below the resulting required space (~5859375 KB) and thus fails. -firmware_dual_602 = [ - {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.0.2h.bin", "size": "2000000000"}}}, - {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.0.2h-cs_64.bin", "size": "3000000000"}}}, -] +firmware_dual_602 = read_data(dir, "firmwareFirmware_dual_602.json") # node-101 has fully downloaded/extracted the target image already. maintUpgJob_node_101_downloaded = [ @@ -47,20 +44,25 @@ ] # Every node in eqptcapacityFSPartition.json has fully pre-downloaded the target image. -maintUpgJob_all_downloaded = [ - {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-102/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, - {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-103/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, - {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-205/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, - {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-206/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, - {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-1002/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, - {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-1001/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, - {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-2002/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, - {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-2003/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, - {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-2001/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, - {"maintUpgJob": {"attributes": {"dn": "topology/pod-2/node-2010/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, - {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-101/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, +maintUpgJob_all_downloaded = read_data(dir, "maintUpgJob_all_downloaded.json") + +# `dn` doesn't match `node_regex` (unparseable): skipped gracefully, not added to the map. +maintUpgJob_malformed_dn = [ + {"maintUpgJob": {"attributes": {"dn": "uni/some/unexpected/format", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, ] +# Only node-101 (one of several failing nodes with the insufficient-space fixture) is pre-downloaded. +maintUpgJob_partial_of_failing = maintUpgJob_node_101_downloaded + +# All 8 nodes that fail with the insufficient-space fixture are pre-downloaded; the +# remaining 3 nodes (2002, 2003, 2010) already have enough free space on their own. +maintUpgJob_all_of_failing = read_data(dir, "maintUpgJob_all_of_failing.json") + +# Mixed real-world response: node-102 and node-103 are present and downloaded, the +# other failing nodes (205, 206, 1002, 1001, 2001, 101) are simply missing from the +# response, and one entry has an empty-string `dn` (unparseable, skipped gracefully). +maintUpgJob_partial_missing_empty = read_data(dir, "maintUpgJob_partial_missing_empty.json") + @pytest.mark.parametrize( "icurl_outputs, cversion, tversion, expected_result", @@ -183,6 +185,55 @@ "6.0(2h)", script.FAIL_UF, ), + # A `maintUpgJob` entry with an unparseable `dn` is skipped gracefully (no crash, + # no false skip): node-101 is not excluded and the check still fails. + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts: maintUpgJob_malformed_dn, + firmware: firmware_dual_602, + }, + "6.0(3a)", + "6.0(2h)", + script.FAIL_UF, + ), + # Multiple nodes fail with the insufficient-space fixture; pre-downloading only + # one of them still leaves the rest failing. + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts: maintUpgJob_partial_of_failing, + firmware: read_data(dir, "firmwareFirmware_dual_image_insufficient.json"), + }, + "5.2(8h)", + "6.1(5e)", + script.FAIL_UF, + ), + # Pre-downloading every node that would otherwise fail with the insufficient-space + # fixture leaves only the already-sufficient nodes, so the result is PASS. + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts: maintUpgJob_all_of_failing, + firmware: read_data(dir, "firmwareFirmware_dual_image_insufficient.json"), + }, + "5.2(8h)", + "6.1(5e)", + script.PASS, + ), + # Mixed response: only node-102/node-103 are covered, the rest of the failing + # nodes are missing from `maintUpgJob`, and an empty-`dn` entry is ignored. + # The uncovered failing nodes (205, 206, 1002, 1001, 2001, 101) still fail. + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts: maintUpgJob_partial_missing_empty, + firmware: read_data(dir, "firmwareFirmware_dual_image_insufficient.json"), + }, + "5.2(8h)", + "6.1(5e)", + script.FAIL_UF, + ), ], ) def test_logic(run_check, mock_icurl, cversion, tversion, expected_result): From 58e4fb3905fe370bc1c1c5b135d8411e31618277 Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Tue, 8 Sep 2026 09:14:05 +0530 Subject: [PATCH 04/21] dynamic bootflash utilization --- aci-preupgrade-validation-script.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 92edcfd4..a321f717 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2179,7 +2179,6 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): download_sts_api = 'maintUpgJob.json' download_sts_api += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100"))' - download_sts_api += '&rsp-subtree=full' try: download_sts = icurl('class', download_sts_api) @@ -2192,7 +2191,7 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): dn = re.search(node_regex, maintUpgJob['maintUpgJob']['attributes']['dn']) if dn: predownloaded_nodes[dn.group("node")] = maintUpgJob['maintUpgJob']['attributes'] - + # Starting 6.0(2a), switch images are shipped as separate 32-bit and 64-bit # isos (`-cs_64` suffix for 64-bit). Below that, only a single 32-bit iso exists. boundary_version = "6.0(2a)" @@ -2214,15 +2213,17 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): if target_size_32 is None: msg = 'Target switch image ({}) not found in Firmware Repository.'.format(switch_target_version) return Result(result=MANUAL, msg=msg, doc_url=doc_url) - required_space = 2 * target_size_32 + required_space = 2 * target_size_32 # only the 32-bit image is ever used pre-6.0(2a) + elif not cversion.older_than(boundary_version) and not tversion.older_than(boundary_version): - # Either image may be used, so size for the larger of the two. + if target_size_32 is None and target_size_64 is None: msg = 'Target switch image(s) not found in Firmware Repository.' return Result(result=MANUAL, msg=msg, doc_url=doc_url) - required_space = 2 * max(target_size_32 or 0, target_size_64 or 0) + required_space = 2 * max(target_size_32 or 0, target_size_64 or 0) # only the larger of the 32-bit or 64-bit image is used post-6.0(2a). + else: - # Crossing the 32/64-bit boundary: both target isos are downloaded while + # Crossing the 32/64-bit boundary: 32 + 64 b target images are downloaded while # the current (32-bit only) image is removed, freeing up its space. if target_size_32 is None and target_size_64 is None: msg = 'Target switch image(s) not found in Firmware Repository.' From 8ff1465529801fc13410738339712be71ab2097d Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Wed, 9 Sep 2026 08:38:40 +0530 Subject: [PATCH 05/21] dynamic bootflash utilization --- aci-preupgrade-validation-script.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index a321f717..8b18c7b2 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2214,6 +2214,7 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): msg = 'Target switch image ({}) not found in Firmware Repository.'.format(switch_target_version) return Result(result=MANUAL, msg=msg, doc_url=doc_url) required_space = 2 * target_size_32 # only the 32-bit image is ever used pre-6.0(2a) + elif not cversion.older_than(boundary_version) and not tversion.older_than(boundary_version): @@ -2221,7 +2222,7 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): msg = 'Target switch image(s) not found in Firmware Repository.' return Result(result=MANUAL, msg=msg, doc_url=doc_url) required_space = 2 * max(target_size_32 or 0, target_size_64 or 0) # only the larger of the 32-bit or 64-bit image is used post-6.0(2a). - + else: # Crossing the 32/64-bit boundary: 32 + 64 b target images are downloaded while # the current (32-bit only) image is removed, freeing up its space. @@ -2234,7 +2235,7 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): msg = 'Current switch image ({}) not found in Firmware Repository.'.format(switch_current_version) return Result(result=MANUAL, msg=msg, doc_url=doc_url) required_space = 2 * ((target_size_32 or 0) + (target_size_64 or 0) - current_size) - + required_space_kb = required_space / 1024.0 # eqptcapacityFSPartition avail/used are in KB for eqptcapacityFSPartition in partitions: From 818c940966d5504f6cba35c698a0bb7fecfcce10 Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Wed, 9 Sep 2026 09:27:57 +0530 Subject: [PATCH 06/21] Untrack .DS_Store and remove orphaned apic_oob_connectivity_check fixtures --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 3 ++ .../fabricNode_1apic.json | 10 ------- .../fabricNode_3apics.json | 26 ------------------ .../fabricPodBlk_none.json | 1 - .../fabricPodBlk_pod1.json | 11 -------- .../fabricPodS_default_all.json | 10 ------- .../fabricPodS_default_and_pod1_range.json | 18 ------------ .../fabricPodS_two_all.json | 18 ------------ .../fabricRsCommPol_default_and_custom.json | 18 ------------ .../fabricRsCommPol_none.json | 1 - .../fabricRsCommPol_to_custom.json | 10 ------- .../fabricRsCommPol_to_default.json | 10 ------- .../fabricRsPodPGrp_default.json | 10 ------- .../fabricRsPodPGrp_default_and_pod1.json | 18 ------------ .../fabricRsPodPGrp_two_all.json | 18 ------------ .../topSystem_mixed_malformed.json | 25 ----------------- .../topSystem_oob_link_local_only.json | 12 -------- .../topSystem_partial_2of3.json | 22 --------------- 19 files changed, 3 insertions(+), 238 deletions(-) delete mode 100644 .DS_Store delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricNode_1apic.json delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricNode_3apics.json delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricPodBlk_none.json delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricPodBlk_pod1.json delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricPodS_default_all.json delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricPodS_default_and_pod1_range.json delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricPodS_two_all.json delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsCommPol_default_and_custom.json delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsCommPol_none.json delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsCommPol_to_custom.json delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsCommPol_to_default.json delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_default.json delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_default_and_pod1.json delete mode 100644 tests/checks/apic_oob_connectivity_check/fabricRsPodPGrp_two_all.json delete mode 100644 tests/checks/apic_oob_connectivity_check/topSystem_mixed_malformed.json delete mode 100644 tests/checks/apic_oob_connectivity_check/topSystem_oob_link_local_only.json delete mode 100644 tests/checks/apic_oob_connectivity_check/topSystem_partial_2of3.json diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Wed, 9 Sep 2026 09:29:27 +0530 Subject: [PATCH 07/21] Sync maintUpgJob query fixture with rsp-subtree=full removal --- .../test_switch_bootflash_usage_check.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py index e58df54a..fbc64e0d 100644 --- a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py +++ b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py @@ -19,7 +19,6 @@ download_sts = 'maintUpgJob.json' download_sts += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100"))' -download_sts += '&rsp-subtree=full' # No pre-downloaded nodes unless a test overrides this key. no_predownload = [] From f9c76d2b839baa978dd73f637500b4667d75f5e4 Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Wed, 9 Sep 2026 15:40:14 +0530 Subject: [PATCH 08/21] dynamic bootflash utilization --- aci-preupgrade-validation-script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 8b18c7b2..11850768 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -38,7 +38,7 @@ import os import re -SCRIPT_VERSION = "v4.2.0" +SCRIPT_VERSION = "v4.2.1" DEFAULT_TIMEOUT = 600 # sec # result constants DONE = 'DONE' From 6d04faad0e875fb979412ddc543bcfa2713a5f5a Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Wed, 9 Sep 2026 17:41:02 +0530 Subject: [PATCH 09/21] 434-dynamically-calculate-the-required-free-space-in-switch-bootflash --- aci-preupgrade-validation-script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 11850768..8b18c7b2 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -38,7 +38,7 @@ import os import re -SCRIPT_VERSION = "v4.2.1" +SCRIPT_VERSION = "v4.2.0" DEFAULT_TIMEOUT = 600 # sec # result constants DONE = 'DONE' From deafe40dbe57d3aa6de0c840072ce1b7faecc7f3 Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Wed, 9 Sep 2026 17:49:11 +0530 Subject: [PATCH 10/21] dynamic bootflash utilization --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 3 --- 2 files changed, 3 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Wed, 9 Sep 2026 20:21:31 +0530 Subject: [PATCH 11/21] dynamic bootflash utilization --- aci-preupgrade-validation-script.py | 3 +- .../test_switch_bootflash_usage_check.py | 39 ++++++++++++------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 8b18c7b2..e03487f3 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2178,7 +2178,8 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): return Result(result=MANUAL, msg='/bootflash directory not found. Check switch health.', doc_url=doc_url) download_sts_api = 'maintUpgJob.json' - download_sts_api += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100"))' + download_sts_api += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100")' + download_sts_api += ',eq(maintUpgJob.desiredVersion,"n9000-1{}"))'.format(tversion.version) try: download_sts = icurl('class', download_sts_api) diff --git a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py index fbc64e0d..c22f01ae 100644 --- a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py +++ b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py @@ -17,8 +17,17 @@ firmware = 'firmwareFirmware.json?query-target-filter=eq(firmwareFirmware.type,"switch")' -download_sts = 'maintUpgJob.json' -download_sts += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100"))' +download_sts_602 = 'maintUpgJob.json' +download_sts_602 += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100")' +download_sts_602 += ',eq(maintUpgJob.desiredVersion,"n9000-16.0(2h)"))' + +download_sts_528 = 'maintUpgJob.json' +download_sts_528 += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100")' +download_sts_528 += ',eq(maintUpgJob.desiredVersion,"n9000-15.2(8h)"))' + +download_sts_615 = 'maintUpgJob.json' +download_sts_615 += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100")' +download_sts_615 += ',eq(maintUpgJob.desiredVersion,"n9000-16.1(5e)"))' # No pre-downloaded nodes unless a test overrides this key. no_predownload = [] @@ -86,7 +95,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: no_predownload, + download_sts_602: no_predownload, firmware: firmware_dual_602, }, "6.0(3a)", @@ -97,7 +106,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: no_predownload, + download_sts_528: no_predownload, firmware: [ {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.15.2.8h.bin", "size": "1000000000"}}}, ], @@ -110,7 +119,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: no_predownload, + download_sts_615: no_predownload, firmware: read_data(dir, "firmwareFirmware_dual_image_insufficient.json"), }, "5.2(8h)", @@ -120,7 +129,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: no_predownload, + download_sts_615: no_predownload, firmware: read_data(dir, "firmwareFirmware_dual_image_sufficient.json"), }, "5.2(8h)", @@ -131,7 +140,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: no_predownload, + download_sts_602: no_predownload, firmware: [], }, "6.0(3a)", @@ -143,7 +152,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: maintUpgJob_node_101_downloaded, + download_sts_602: maintUpgJob_node_101_downloaded, firmware: firmware_dual_602, }, "6.0(3a)", @@ -154,7 +163,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: maintUpgJob_node_999_downloaded, + download_sts_602: maintUpgJob_node_999_downloaded, firmware: firmware_dual_602, }, "6.0(3a)", @@ -165,7 +174,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: maintUpgJob_all_downloaded, + download_sts_602: maintUpgJob_all_downloaded, firmware: firmware_dual_602, }, "6.0(3a)", @@ -177,7 +186,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: old_ver_no_prop, + download_sts_602: old_ver_no_prop, firmware: firmware_dual_602, }, "6.0(3a)", @@ -189,7 +198,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: maintUpgJob_malformed_dn, + download_sts_602: maintUpgJob_malformed_dn, firmware: firmware_dual_602, }, "6.0(3a)", @@ -201,7 +210,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: maintUpgJob_partial_of_failing, + download_sts_615: maintUpgJob_partial_of_failing, firmware: read_data(dir, "firmwareFirmware_dual_image_insufficient.json"), }, "5.2(8h)", @@ -213,7 +222,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: maintUpgJob_all_of_failing, + download_sts_615: maintUpgJob_all_of_failing, firmware: read_data(dir, "firmwareFirmware_dual_image_insufficient.json"), }, "5.2(8h)", @@ -226,7 +235,7 @@ ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), - download_sts: maintUpgJob_partial_missing_empty, + download_sts_615: maintUpgJob_partial_missing_empty, firmware: read_data(dir, "firmwareFirmware_dual_image_insufficient.json"), }, "5.2(8h)", From 5f912392495bd25d0e853e67cc8992d91dfe9668 Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Wed, 9 Sep 2026 20:40:04 +0530 Subject: [PATCH 12/21] dynamic bootflash utilization --- aci-preupgrade-validation-script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index e03487f3..20228ec7 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2179,7 +2179,7 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): download_sts_api = 'maintUpgJob.json' download_sts_api += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100")' - download_sts_api += ',eq(maintUpgJob.desiredVersion,"n9000-1{}"))'.format(tversion.version) + download_sts_api += ',eq(maintUpgJob.desiredVersion,"n9000-1{}"))'.format(tversion) try: download_sts = icurl('class', download_sts_api) From aa82f357f62aab8eb6343531bbe4ef494cdb744d Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Thu, 10 Sep 2026 08:13:04 +0530 Subject: [PATCH 13/21] dynamic bootflash utilization --- aci-preupgrade-validation-script.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 20228ec7..ba3c2fb0 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2172,26 +2172,25 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): partitions_api = 'eqptcapacityFSPartition.json' partitions_api += '?query-target-filter=eq(eqptcapacityFSPartition.path,"/bootflash")' - partitions = icurl('class', partitions_api) + download_sts_api = 'maintUpgJob.json' + download_sts_api += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded")' + download_sts_api += ',eq(maintUpgJob.desiredVersion,"n9000-1{}"))'.format(tversion) + partitions = icurl('class', partitions_api) if not partitions: return Result(result=MANUAL, msg='/bootflash directory not found. Check switch health.', doc_url=doc_url) - download_sts_api = 'maintUpgJob.json' - download_sts_api += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100")' - download_sts_api += ',eq(maintUpgJob.desiredVersion,"n9000-1{}"))'.format(tversion) - + predownloaded_nodes = [] try: download_sts = icurl('class', download_sts_api) except OldVerPropNotFound: - # Older versions don't have 'dnldStatus'/'dnldPercent' params + # Older versions don't have 'dnldStatus' param download_sts = [] - - predownloaded_nodes = {} + for maintUpgJob in download_sts: dn = re.search(node_regex, maintUpgJob['maintUpgJob']['attributes']['dn']) - if dn: - predownloaded_nodes[dn.group("node")] = maintUpgJob['maintUpgJob']['attributes'] + node = dn.group("node") + predownloaded_nodes.append(node) # Starting 6.0(2a), switch images are shipped as separate 32-bit and 64-bit # isos (`-cs_64` suffix for 64-bit). Below that, only a single 32-bit iso exists. From b3c28ce39a685ddb9943f0114b1e6c74fa0cf2e4 Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Thu, 10 Sep 2026 08:38:31 +0530 Subject: [PATCH 14/21] dynamic bootflash utilization --- aci-preupgrade-validation-script.py | 26 +++++-- .../test_switch_bootflash_usage_check.py | 74 ++++++++++++++++++- 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index ba3c2fb0..0029d14f 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2189,8 +2189,8 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): for maintUpgJob in download_sts: dn = re.search(node_regex, maintUpgJob['maintUpgJob']['attributes']['dn']) - node = dn.group("node") - predownloaded_nodes.append(node) + if dn: + predownloaded_nodes.append(dn.group("node")) # Starting 6.0(2a), switch images are shipped as separate 32-bit and 64-bit # isos (`-cs_64` suffix for 64-bit). Below that, only a single 32-bit iso exists. @@ -2218,23 +2218,37 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): elif not cversion.older_than(boundary_version) and not tversion.older_than(boundary_version): + # The larger image is used as a conservative estimate, so both sizes must be + # known; a missing one can't be assumed to be the smaller (or zero-byte) one. if target_size_32 is None and target_size_64 is None: - msg = 'Target switch image(s) not found in Firmware Repository.' + msg = 'Target switch images ({}, {}) not found in Firmware Repository.'.format(switch_target_version, switch_target_version_64) + return Result(result=MANUAL, msg=msg, doc_url=doc_url) + elif target_size_32 is None: + msg = '32-bit target switch image ({}) not found in Firmware Repository.'.format(switch_target_version) return Result(result=MANUAL, msg=msg, doc_url=doc_url) - required_space = 2 * max(target_size_32 or 0, target_size_64 or 0) # only the larger of the 32-bit or 64-bit image is used post-6.0(2a). + elif target_size_64 is None: + msg = '64-bit target switch image ({}) not found in Firmware Repository.'.format(switch_target_version_64) + return Result(result=MANUAL, msg=msg, doc_url=doc_url) + required_space = 2 * max(target_size_32, target_size_64) # only the larger of the 32-bit or 64-bit image is used post-6.0(2a). else: # Crossing the 32/64-bit boundary: 32 + 64 b target images are downloaded while # the current (32-bit only) image is removed, freeing up its space. if target_size_32 is None and target_size_64 is None: - msg = 'Target switch image(s) not found in Firmware Repository.' + msg = 'Target switch images ({}, {}) not found in Firmware Repository.'.format(switch_target_version, switch_target_version_64) + return Result(result=MANUAL, msg=msg, doc_url=doc_url) + elif target_size_32 is None: + msg = '32-bit target switch image ({}) not found in Firmware Repository.'.format(switch_target_version) + return Result(result=MANUAL, msg=msg, doc_url=doc_url) + elif target_size_64 is None: + msg = '64-bit target switch image ({}) not found in Firmware Repository.'.format(switch_target_version_64) return Result(result=MANUAL, msg=msg, doc_url=doc_url) switch_current_version = "aci-n9000-dk9.1{}.bin".format(cversion.dot_version) current_size = fw_sizes.get(switch_current_version) if current_size is None: msg = 'Current switch image ({}) not found in Firmware Repository.'.format(switch_current_version) return Result(result=MANUAL, msg=msg, doc_url=doc_url) - required_space = 2 * ((target_size_32 or 0) + (target_size_64 or 0) - current_size) + required_space = 2 * (target_size_32 + target_size_64 - current_size) required_space_kb = required_space / 1024.0 # eqptcapacityFSPartition avail/used are in KB diff --git a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py index c22f01ae..cfef2040 100644 --- a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py +++ b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py @@ -18,15 +18,15 @@ firmware = 'firmwareFirmware.json?query-target-filter=eq(firmwareFirmware.type,"switch")' download_sts_602 = 'maintUpgJob.json' -download_sts_602 += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100")' +download_sts_602 += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded")' download_sts_602 += ',eq(maintUpgJob.desiredVersion,"n9000-16.0(2h)"))' download_sts_528 = 'maintUpgJob.json' -download_sts_528 += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100")' +download_sts_528 += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded")' download_sts_528 += ',eq(maintUpgJob.desiredVersion,"n9000-15.2(8h)"))' download_sts_615 = 'maintUpgJob.json' -download_sts_615 += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded"),eq(maintUpgJob.dnldPercent,"100")' +download_sts_615 += '?query-target-filter=and(eq(maintUpgJob.dnldStatus,"downloaded")' download_sts_615 += ',eq(maintUpgJob.desiredVersion,"n9000-16.1(5e)"))' # No pre-downloaded nodes unless a test overrides this key. @@ -41,6 +41,30 @@ # falls below the resulting required space (~5859375 KB) and thus fails. firmware_dual_602 = read_data(dir, "firmwareFirmware_dual_602.json") +# Only the 64-bit target image (6.0(2h)) is missing from the Firmware Repository. +firmware_602_missing_64 = [ + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.0.2h.bin", "size": "2000000000"}}}, +] + +# Only the 32-bit target image (6.0(2h)) is missing from the Firmware Repository. +firmware_602_missing_32 = [ + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.0.2h-cs_64.bin", "size": "3000000000"}}}, +] + +# Crossing 6.0(2a): current (5.2(8h)) image plus only the 32-bit target (6.1(5e)); the +# 64-bit target image is missing from the Firmware Repository. +firmware_crossing_missing_64 = [ + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.15.2.8h.bin", "size": "2000000000"}}}, + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.1.5e.bin", "size": "3000000000"}}}, +] + +# Crossing 6.0(2a): current (5.2(8h)) image plus only the 64-bit target (6.1(5e)); the +# 32-bit target image is missing from the Firmware Repository. +firmware_crossing_missing_32 = [ + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.15.2.8h.bin", "size": "2000000000"}}}, + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.1.5e-cs_64.bin", "size": "3000000000"}}}, +] + # node-101 has fully downloaded/extracted the target image already. maintUpgJob_node_101_downloaded = [ {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-101/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, @@ -147,6 +171,50 @@ "6.0(2h)", script.MANUAL, ), + # Post-6.0(2a): only the 64-bit target image is missing from the Firmware Repository. + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts_602: no_predownload, + firmware: firmware_602_missing_64, + }, + "6.0(3a)", + "6.0(2h)", + script.MANUAL, + ), + # Post-6.0(2a): only the 32-bit target image is missing from the Firmware Repository. + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts_602: no_predownload, + firmware: firmware_602_missing_32, + }, + "6.0(3a)", + "6.0(2h)", + script.MANUAL, + ), + # Crossing 6.0(2a): only the 64-bit target image is missing from the Firmware Repository. + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts_615: no_predownload, + firmware: firmware_crossing_missing_64, + }, + "5.2(8h)", + "6.1(5e)", + script.MANUAL, + ), + # Crossing 6.0(2a): only the 32-bit target image is missing from the Firmware Repository. + ( + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts_615: no_predownload, + firmware: firmware_crossing_missing_32, + }, + "5.2(8h)", + "6.1(5e)", + script.MANUAL, + ), # node-101 (the only node that would otherwise fail) already fully downloaded # the target image, so it's excluded from the check and the result is PASS. ( From 38b870c3ff10698e4ddabcabeae7d20f0c208df0 Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Thu, 10 Sep 2026 08:47:26 +0530 Subject: [PATCH 15/21] dynamic bootflash utilization --- docs/docs/validations.md | 65 +++++----------------------------------- 1 file changed, 7 insertions(+), 58 deletions(-) diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 717c1757..472971fc 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -651,67 +651,16 @@ To prevent this, check the `/bootflash` prior to an upgrade and take the necessa The pre-upgrade validation built into Cisco APIC upgrade workflow monitors the fault F1821, which can capture the high utilization of any partition. When this fault is present, we recommend that you resolve it prior to the upgrade even if the fault is not for bootflash. -The ACI Pre-Upgrade Validation script (this script) focuses on the utilization of bootflash on each switch specifically to see if there are any issues with bootflash where the usage is more than 50%, which might trigger the internal cleanup script. - -!!! example "Example of a query used by this script" - The script is calculating the bootflash usage using `avail` and `used` in the object `eqptcapacityFSPartition` for each switch. - ``` - f2-apic1# moquery -c eqptcapacityFSPartition -f 'eqptcapacity.FSPartition.path=="/bootflash"' - Total Objects shown: 6 - - # eqptcapacity.FSPartition - name : bootflash - avail : 7214920 - childAction : - dn : topology/pod-1/node-101/sys/eqptcapacity/fspartition-bootflash - memAlert : normal - modTs : never - monPolDn : uni/fabric/monfab-default - path : /bootflash - rn : fspartition-bootflash - status : - used : 4320184 - --- omit --- - ``` +The ACI Pre-Upgrade Validation script (this script) dynamically calculates the actual bootflash space required for the target upgrade, rather than relying on a fixed usage threshold, and compares it against each switch's available `/bootflash` space: -!!! tip - Alternatively you can log into a leaf switch CLI, and check `/bootflash` usage `df -h` - ``` - leaf1# df -h - Filesystem Size Used Avail Use% Mounted on - rootfs 2.5G 935M 1.6G 38% /bin - /dev/sda4 12G 5.7G 4.9G 54% /bootflash - /dev/sda2 4.7G 9.6M 4.4G 1% /recovery - /dev/mapper/map-sda9 11G 5.7G 4.2G 58% /isan/lib - none 3.0G 602M 2.5G 20% /dev/shm - none 50M 3.4M 47M 7% /etc - /dev/sda6 56M 1.3M 50M 3% /mnt/cfg/1 - /dev/sda5 56M 1.3M 50M 3% /mnt/cfg/0 - /dev/sda8 15G 140M 15G 1% /mnt/ifc/log - /dev/sda3 115M 52M 54M 50% /mnt/pss - none 1.5G 2.3M 1.5G 1% /tmp - none 50M 240K 50M 1% /var/log - /dev/sda7 12G 1.4G 9.3G 13% /logflash - none 350M 54M 297M 16% /var/log/dme/log/dme_logs - none 512M 24M 489M 5% /var/sysmgr/mem_logs - none 40M 4.0K 40M 1% /var/sysmgr/startup-cfg - none 500M 0 500M 0% /volatile - ``` +* Required space is based on the target image size(s) needed to download and extract on top of the existing content. Starting 6.0(2a), switch images are shipped as separate 32-bit and 64-bit isos, so both current and target version determine whether one or both images apply: + * Both versions pre-6.0(2a): only the single 32-bit image size is used. + * Both versions post-6.0(2a): the larger of the 32-bit/64-bit target images is used. + * Crossing the 6.0(2a) boundary: both target images are downloaded while the current (32-bit-only) image is removed, freeing its space. -!!! note - If you suspect that the auto cleanup removed some files within `/bootflash`, you can review a log to validate this: +* Nodes that already pre-downloaded the exact target version (`maintUpgJob.dnldStatus == downloaded` and `desiredVersion` matching target) are excluded from the check, since no further download/extraction is needed. - ``` - leaf1# egrep "higher|removed" /mnt/pss/core_control.log - [2020-07-22 16:52:08.928318] Bootflash Usage is higher than 50%!! - [2020-07-22 16:52:08.931990] File: MemoryLog.65%_usage removed !! - [2020-07-22 16:52:08.943914] File: mem_log.txt.old.gz removed !! - [2020-07-22 16:52:08.955376] File: libmon.logs removed !! - [2020-07-22 16:52:08.966686] File: urib_api_log.txt removed !! - [2020-07-22 16:52:08.977832] File: disk_log.txt removed !! - [2020-07-22 16:52:08.989102] File: mem_log.txt removed !! - [2020-07-22 16:52:09.414572] File: aci-n9000-dk9.13.2.1m.bin removed !! - ``` +* If a required firmware image isn't found in the Firmware Repository, the check reports a manual review. ### APIC SSD Health From effe99eeb042142bf673daddde78eb719b2d865b Mon Sep 17 00:00:00 2001 From: lovkeshsharma702 Date: Mon, 14 Sep 2026 16:40:45 +0530 Subject: [PATCH 16/21] dynamic bootflash utilization --- aci-preupgrade-validation-script.py | 73 +++++++++++-------- docs/docs/validations.md | 4 +- .../test_switch_bootflash_usage_check.py | 71 +++++++++++++++++- 3 files changed, 111 insertions(+), 37 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 0029d14f..50eda4dc 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2158,7 +2158,7 @@ def switch_group_guideline_check(fabric_nodes, **kwargs): @check_wrapper(check_title="Switch Node /bootflash usage") -def switch_bootflash_usage_check(cversion, tversion, **kwargs): +def switch_bootflash_usage_check(sw_cversion, tversion, **kwargs): result = FAIL_UF msg = '' headers = ["Pod-ID", "Node-ID", "Avail (MB)", "Required (MB)"] @@ -2166,6 +2166,9 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): recommended_action = "Insufficient free space to download and extract the target image! Contact Cisco TAC for Support" doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#switch-node-bootflash-usage" + if not sw_cversion: + return Result(result=MANUAL, msg="Current switch version not found. Check switch health.", doc_url=doc_url) + if not tversion: return Result(result=MANUAL, msg=TVER_MISSING, doc_url=doc_url) @@ -2208,16 +2211,21 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): target_size_32 = fw_sizes.get(switch_target_version) target_size_64 = fw_sizes.get(switch_target_version_64) - if cversion.older_than(boundary_version) and tversion.older_than(boundary_version): - # Only the 32-bit image is ever used pre-6.0(2a). + # sw_cversion (lowest switch version), not the APIC cversion, drives the boundary + # decision: the upgrade guide has APICs reach 6.0(2a)+ before the switches, so the + # switches can still be pre-boundary while the APIC cluster is already post-boundary. + target_is_legacy = tversion.older_than(boundary_version) + current_is_legacy = sw_cversion.older_than(boundary_version) + + if target_is_legacy: + # Only the 32-bit image is ever used for a pre-6.0(2a) target. if target_size_32 is None: msg = 'Target switch image ({}) not found in Firmware Repository.'.format(switch_target_version) return Result(result=MANUAL, msg=msg, doc_url=doc_url) - required_space = 2 * target_size_32 # only the 32-bit image is ever used pre-6.0(2a) - - - elif not cversion.older_than(boundary_version) and not tversion.older_than(boundary_version): + required_space = 2 * target_size_32 + downloaded_required_space = target_size_32 + else: # The larger image is used as a conservative estimate, so both sizes must be # known; a missing one can't be assumed to be the smaller (or zero-byte) one. if target_size_32 is None and target_size_64 is None: @@ -2229,39 +2237,40 @@ def switch_bootflash_usage_check(cversion, tversion, **kwargs): elif target_size_64 is None: msg = '64-bit target switch image ({}) not found in Firmware Repository.'.format(switch_target_version_64) return Result(result=MANUAL, msg=msg, doc_url=doc_url) - required_space = 2 * max(target_size_32, target_size_64) # only the larger of the 32-bit or 64-bit image is used post-6.0(2a). - - else: - # Crossing the 32/64-bit boundary: 32 + 64 b target images are downloaded while - # the current (32-bit only) image is removed, freeing up its space. - if target_size_32 is None and target_size_64 is None: - msg = 'Target switch images ({}, {}) not found in Firmware Repository.'.format(switch_target_version, switch_target_version_64) - return Result(result=MANUAL, msg=msg, doc_url=doc_url) - elif target_size_32 is None: - msg = '32-bit target switch image ({}) not found in Firmware Repository.'.format(switch_target_version) - return Result(result=MANUAL, msg=msg, doc_url=doc_url) - elif target_size_64 is None: - msg = '64-bit target switch image ({}) not found in Firmware Repository.'.format(switch_target_version_64) - return Result(result=MANUAL, msg=msg, doc_url=doc_url) - switch_current_version = "aci-n9000-dk9.1{}.bin".format(cversion.dot_version) - current_size = fw_sizes.get(switch_current_version) - if current_size is None: - msg = 'Current switch image ({}) not found in Firmware Repository.'.format(switch_current_version) - return Result(result=MANUAL, msg=msg, doc_url=doc_url) - required_space = 2 * (target_size_32 + target_size_64 - current_size) - + + downloaded_required_space = max(target_size_32, target_size_64) + + if current_is_legacy: + # Crossing the 32/64-bit boundary: the pre-6.0(2a) switch only ever had a + # 32-bit image, so its size is freed once removed during the upgrade. + switch_current_version = "aci-n9000-dk9.1{}.bin".format(sw_cversion.dot_version) + current_size = fw_sizes.get(switch_current_version) + if current_size is None: + msg = 'Current switch image ({}) not found in Firmware Repository.'.format(switch_current_version) + return Result(result=MANUAL, msg=msg, doc_url=doc_url) + if target_size_32 > current_size: + required_space = 2 * (target_size_32 + target_size_64 - current_size) + else: + required_space = 2 * max(target_size_32, target_size_64) + else: + required_space = 2 * max(target_size_32, target_size_64) + required_space_kb = required_space / 1024.0 # eqptcapacityFSPartition avail/used are in KB + downloaded_required_space_kb = downloaded_required_space / 1024.0 for eqptcapacityFSPartition in partitions: dn = re.search(node_regex, eqptcapacityFSPartition['eqptcapacityFSPartition']['attributes']['dn']) pod = dn.group("pod") node = dn.group("node") - if node in predownloaded_nodes: - continue avail = int(eqptcapacityFSPartition['eqptcapacityFSPartition']['attributes']['avail']) - if avail < required_space_kb: - data.append([pod, node, round(avail / 1024.0, 2), round(required_space_kb / 1024.0, 2)]) + # dnldStatus == downloaded only proves the image was delivered, not that + # extraction (which still consumes bootflash) has completed, so a downloaded + # node is still checked, just against the smaller extraction-only requirement. + node_required_space_kb = downloaded_required_space_kb if node in predownloaded_nodes else required_space_kb + + if avail < node_required_space_kb: + data.append([pod, node, round(avail / 1024.0, 2), round(node_required_space_kb / 1024.0, 2)]) if not data: result = PASS diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 472971fc..25463e1d 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -656,9 +656,9 @@ The ACI Pre-Upgrade Validation script (this script) dynamically calculates the a * Required space is based on the target image size(s) needed to download and extract on top of the existing content. Starting 6.0(2a), switch images are shipped as separate 32-bit and 64-bit isos, so both current and target version determine whether one or both images apply: * Both versions pre-6.0(2a): only the single 32-bit image size is used. * Both versions post-6.0(2a): the larger of the 32-bit/64-bit target images is used. - * Crossing the 6.0(2a) boundary: both target images are downloaded while the current (32-bit-only) image is removed, freeing its space. + * Crossing the 6.0(2a) boundary: target image downloaded while the current is removed, freeing its space for successful extraction. -* Nodes that already pre-downloaded the exact target version (`maintUpgJob.dnldStatus == downloaded` and `desiredVersion` matching target) are excluded from the check, since no further download/extraction is needed. +* Nodes that already downloaded the exact target version (`maintUpgJob.dnldStatus == downloaded` and `desiredVersion` matching target) are still evaluated, just against a smaller, extraction-only requirement (the larger of the 32-bit/64-bit target image sizes, without doubling for the download), since extraction and later upgrade stages can still require additional bootflash space. * If a required firmware image isn't found in the Firmware Repository, the check reports a manual review. diff --git a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py index cfef2040..9d025874 100644 --- a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py +++ b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py @@ -95,9 +95,67 @@ # response, and one entry has an empty-string `dn` (unparseable, skipped gracefully). maintUpgJob_partial_missing_empty = read_data(dir, "maintUpgJob_partial_missing_empty.json") +# APIC (cversion) reaches 6.0(2a)+ before the switches per the documented upgrade +# sequence, so sw_cversion (5.2(8h), pre-boundary) must still drive the crossing decision +# even though the APIC cluster (cversion 6.0(3a)) is already post-boundary. cversion is +# passed here (as it would be via query_common_data() in production) solely to prove the +# check ignores it: switch_bootflash_usage_check() only declares sw_cversion/tversion, so +# cversion lands in **kwargs and has no effect on the result. Sizes are chosen so +# target_size_32 (3 GiB) exceeds current_size (2 GiB), forcing the full crossing formula: +# 2 * (3 GiB + 3 GiB - 2 GiB) = 8 GiB required. +apic_post_boundary_switch_pre_boundary_case = [ + { + partitions: read_data(dir, "eqptcapacityFSPartition.json"), + download_sts_615: no_predownload, + firmware: [ + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.15.2.8h.bin", "size": str(2 * 1024 ** 3)}}}, + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.1.5e.bin", "size": str(3 * 1024 ** 3)}}}, + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.1.5e-cs_64.bin", "size": str(3 * 1024 ** 3)}}}, + ], + }, +] + + +@pytest.mark.parametrize("icurl_outputs", apic_post_boundary_switch_pre_boundary_case) +def test_apic_post_boundary_switch_pre_boundary_uses_crossing_formula(run_check, mock_icurl): + result = run_check( + cversion=script.AciVersion("6.0(3a)"), + sw_cversion=script.AciVersion("5.2(8h)"), + tversion=script.AciVersion("6.1(5e)"), + ) + assert result.result == script.FAIL_UF + assert result.data + required_mb = str(8 * 1024.0) # "8192.0" MB == 8 GiB + assert all(row[3] == required_mb for row in result.data) + + +#`dnldStatus == "downloaded"` only proves the image was delivered, not +# that extraction (which still consumes bootflash) succeeded, so node-101 having +# pre-downloaded the exact target must still fail when its remaining space (1 KB) can't +# fit the extraction-only requirement (max(target_size_32, target_size_64) == 3 GB). +exact_target_downloaded_insufficient_case = [ + { + partitions: [ + {"eqptcapacityFSPartition": {"attributes": {"dn": "topology/pod-1/node-101/sys/eqptcapacity/fspartition-bootflash", "avail": "1", "used": "999999999"}}}, + ], + download_sts_602: maintUpgJob_node_101_downloaded, + firmware: firmware_dual_602, + }, +] + + +@pytest.mark.parametrize("icurl_outputs", exact_target_downloaded_insufficient_case) +def test_exact_target_downloaded_still_fails_on_insufficient_extraction_space(run_check, mock_icurl): + result = run_check( + sw_cversion=script.AciVersion("6.0(3a)"), + tversion=script.AciVersion("6.0(2h)"), + ) + assert result.result == script.FAIL_UF + assert result.data == [["1", "101", "0.0", "2861.02"]] + @pytest.mark.parametrize( - "icurl_outputs, cversion, tversion, expected_result", + "icurl_outputs, sw_cversion, tversion, expected_result", [ # No tversion provided. ( @@ -106,6 +164,13 @@ None, script.MANUAL, ), + # No sw_cversion (lowest switch version) found. + ( + {}, + None, + "6.0(2h)", + script.MANUAL, + ), # /bootflash partition objects not found at all. Returns before maintUpgJob is queried. ( { @@ -312,9 +377,9 @@ ), ], ) -def test_logic(run_check, mock_icurl, cversion, tversion, expected_result): +def test_logic(run_check, mock_icurl, sw_cversion, tversion, expected_result): result = run_check( - cversion=script.AciVersion(cversion) if cversion else None, + sw_cversion=script.AciVersion(sw_cversion) if sw_cversion else None, tversion=script.AciVersion(tversion) if tversion else None, ) assert result.result == expected_result From b0007a95d9e5b2c4b7eeea7bb2d16cec7075dda0 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 15 Sep 2026 09:22:51 -0400 Subject: [PATCH 17/21] fix: prioritize missing target version Ensure bootflash validation reports the established target-version message before checking switch-version availability, with focused regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- aci-preupgrade-validation-script.py | 6 +++--- .../test_switch_bootflash_usage_check.py | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 50eda4dc..7e82085c 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2166,12 +2166,12 @@ def switch_bootflash_usage_check(sw_cversion, tversion, **kwargs): recommended_action = "Insufficient free space to download and extract the target image! Contact Cisco TAC for Support" doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#switch-node-bootflash-usage" - if not sw_cversion: - return Result(result=MANUAL, msg="Current switch version not found. Check switch health.", doc_url=doc_url) - if not tversion: return Result(result=MANUAL, msg=TVER_MISSING, doc_url=doc_url) + if not sw_cversion: + return Result(result=MANUAL, msg="Current switch version not found. Check switch health.", doc_url=doc_url) + partitions_api = 'eqptcapacityFSPartition.json' partitions_api += '?query-target-filter=eq(eqptcapacityFSPartition.path,"/bootflash")' diff --git a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py index 9d025874..a72b9e7a 100644 --- a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py +++ b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py @@ -154,13 +154,20 @@ def test_exact_target_downloaded_still_fails_on_insufficient_extraction_space(ru assert result.data == [["1", "101", "0.0", "2861.02"]] +def test_missing_target_version_takes_precedence(run_check): + result = run_check(sw_cversion=None, tversion=None) + + assert result.result == script.MANUAL + assert result.msg == script.TVER_MISSING + + @pytest.mark.parametrize( "icurl_outputs, sw_cversion, tversion, expected_result", [ # No tversion provided. ( {}, - None, + "6.0(3a)", None, script.MANUAL, ), From 82f5d24ffc20c7d1b441b4d4a850829c1c4bb1b9 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 15 Sep 2026 09:34:17 -0400 Subject: [PATCH 18/21] fix: calculate remaining crossing space Distinguish pre-split and split-image upgrade scenarios, credit only the staged 32-bit image during boundary crossings, and document the API-based calculations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- aci-preupgrade-validation-script.py | 17 +++-- docs/docs/validations.md | 15 ++-- .../test_switch_bootflash_usage_check.py | 76 ++++++++++++++----- 3 files changed, 77 insertions(+), 31 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 7e82085c..21389d37 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2211,13 +2211,13 @@ def switch_bootflash_usage_check(sw_cversion, tversion, **kwargs): target_size_32 = fw_sizes.get(switch_target_version) target_size_64 = fw_sizes.get(switch_target_version_64) - # sw_cversion (lowest switch version), not the APIC cversion, drives the boundary + # sw_cversion (lowest switch version), not the APIC cversion, drives the image split # decision: the upgrade guide has APICs reach 6.0(2a)+ before the switches, so the - # switches can still be pre-boundary while the APIC cluster is already post-boundary. - target_is_legacy = tversion.older_than(boundary_version) - current_is_legacy = sw_cversion.older_than(boundary_version) + # switches can still be pre-split while the APIC cluster is in the split-image era. + target_is_pre_split = tversion.older_than(boundary_version) + current_is_pre_split = sw_cversion.older_than(boundary_version) - if target_is_legacy: + if target_is_pre_split: # Only the 32-bit image is ever used for a pre-6.0(2a) target. if target_size_32 is None: msg = 'Target switch image ({}) not found in Firmware Repository.'.format(switch_target_version) @@ -2238,9 +2238,7 @@ def switch_bootflash_usage_check(sw_cversion, tversion, **kwargs): msg = '64-bit target switch image ({}) not found in Firmware Repository.'.format(switch_target_version_64) return Result(result=MANUAL, msg=msg, doc_url=doc_url) - downloaded_required_space = max(target_size_32, target_size_64) - - if current_is_legacy: + if current_is_pre_split: # Crossing the 32/64-bit boundary: the pre-6.0(2a) switch only ever had a # 32-bit image, so its size is freed once removed during the upgrade. switch_current_version = "aci-n9000-dk9.1{}.bin".format(sw_cversion.dot_version) @@ -2252,8 +2250,11 @@ def switch_bootflash_usage_check(sw_cversion, tversion, **kwargs): required_space = 2 * (target_size_32 + target_size_64 - current_size) else: required_space = 2 * max(target_size_32, target_size_64) + # A downloaded 32-bit target is already reflected in the current `avail`. + downloaded_required_space = required_space - target_size_32 else: required_space = 2 * max(target_size_32, target_size_64) + downloaded_required_space = max(target_size_32, target_size_64) required_space_kb = required_space / 1024.0 # eqptcapacityFSPartition avail/used are in KB downloaded_required_space_kb = downloaded_required_space / 1024.0 diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 25463e1d..80486135 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -651,14 +651,17 @@ To prevent this, check the `/bootflash` prior to an upgrade and take the necessa The pre-upgrade validation built into Cisco APIC upgrade workflow monitors the fault F1821, which can capture the high utilization of any partition. When this fault is present, we recommend that you resolve it prior to the upgrade even if the fault is not for bootflash. -The ACI Pre-Upgrade Validation script (this script) dynamically calculates the actual bootflash space required for the target upgrade, rather than relying on a fixed usage threshold, and compares it against each switch's available `/bootflash` space: +The ACI Pre-Upgrade Validation script (this script) dynamically calculates the actual bootflash space required for the target upgrade, rather than relying on a fixed usage threshold, and compares it against each switch's available `/bootflash` space. Starting with 6.0(2a), switch releases use separate 32-bit and 64-bit images. The calculation therefore distinguishes between pre-split releases older than 6.0(2a) and split-image releases starting with 6.0(2a): -* Required space is based on the target image size(s) needed to download and extract on top of the existing content. Starting 6.0(2a), switch images are shipped as separate 32-bit and 64-bit isos, so both current and target version determine whether one or both images apply: - * Both versions pre-6.0(2a): only the single 32-bit image size is used. - * Both versions post-6.0(2a): the larger of the 32-bit/64-bit target images is used. - * Crossing the 6.0(2a) boundary: target image downloaded while the current is removed, freeing its space for successful extraction. +* A pre-split target requires twice the 32-bit target image size. +* An upgrade from a pre-split release to a split-image release requires `2 × (32-bit target size + 64-bit target size − current image size)`. If the 32-bit target is not larger than the current image, the check instead uses twice the larger target image. +* An upgrade where both the current and target releases are in the split-image era requires twice the larger target image. -* Nodes that already downloaded the exact target version (`maintUpgJob.dnldStatus == downloaded` and `desiredVersion` matching target) are still evaluated, just against a smaller, extraction-only requirement (the larger of the 32-bit/64-bit target image sizes, without doubling for the download), since extraction and later upgrade stages can still require additional bootflash space. +Nodes that already downloaded the exact target version (`maintUpgJob.dnldStatus == downloaded` and `desiredVersion` matching target) are still evaluated because extraction and later upgrade stages require additional bootflash space. Since the downloaded image is already reflected in the current available-space value: + +* A pre-split target requires one 32-bit target image of remaining space. +* A boundary-crossing upgrade requires the full crossing requirement minus the downloaded 32-bit target image. +* A split-image-era upgrade requires the larger target image of remaining space. * If a required firmware image isn't found in the Firmware Repository, the check reports a manual review. diff --git a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py index a72b9e7a..386c918e 100644 --- a/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py +++ b/tests/checks/switch_bootflash_usage_check/test_switch_bootflash_usage_check.py @@ -65,7 +65,7 @@ {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.1.5e-cs_64.bin", "size": "3000000000"}}}, ] -# node-101 has fully downloaded/extracted the target image already. +# node-101 has downloaded the exact target image; extraction status is unknown. maintUpgJob_node_101_downloaded = [ {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-101/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, ] @@ -75,7 +75,7 @@ {"maintUpgJob": {"attributes": {"dn": "topology/pod-1/node-999/sys/maintupgjob", "dnldStatus": "downloaded", "dnldPercent": "100"}}}, ] -# Every node in eqptcapacityFSPartition.json has fully pre-downloaded the target image. +# Every node in eqptcapacityFSPartition.json has downloaded the exact target image. maintUpgJob_all_downloaded = read_data(dir, "maintUpgJob_all_downloaded.json") # `dn` doesn't match `node_regex` (unparseable): skipped gracefully, not added to the map. @@ -96,28 +96,30 @@ maintUpgJob_partial_missing_empty = read_data(dir, "maintUpgJob_partial_missing_empty.json") # APIC (cversion) reaches 6.0(2a)+ before the switches per the documented upgrade -# sequence, so sw_cversion (5.2(8h), pre-boundary) must still drive the crossing decision -# even though the APIC cluster (cversion 6.0(3a)) is already post-boundary. cversion is +# sequence, so sw_cversion (5.2(8h), pre-split) must still drive the crossing decision +# even though the APIC cluster (cversion 6.0(3a)) is already in the split-image era. cversion is # passed here (as it would be via query_common_data() in production) solely to prove the # check ignores it: switch_bootflash_usage_check() only declares sw_cversion/tversion, so # cversion lands in **kwargs and has no effect on the result. Sizes are chosen so # target_size_32 (3 GiB) exceeds current_size (2 GiB), forcing the full crossing formula: # 2 * (3 GiB + 3 GiB - 2 GiB) = 8 GiB required. -apic_post_boundary_switch_pre_boundary_case = [ +firmware_crossing_2_3_3 = [ + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.15.2.8h.bin", "size": str(2 * 1024 ** 3)}}}, + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.1.5e.bin", "size": str(3 * 1024 ** 3)}}}, + {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.1.5e-cs_64.bin", "size": str(3 * 1024 ** 3)}}}, +] + +apic_split_image_switch_pre_split_case = [ { partitions: read_data(dir, "eqptcapacityFSPartition.json"), download_sts_615: no_predownload, - firmware: [ - {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.15.2.8h.bin", "size": str(2 * 1024 ** 3)}}}, - {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.1.5e.bin", "size": str(3 * 1024 ** 3)}}}, - {"firmwareFirmware": {"attributes": {"isoname": "aci-n9000-dk9.16.1.5e-cs_64.bin", "size": str(3 * 1024 ** 3)}}}, - ], + firmware: firmware_crossing_2_3_3, }, ] -@pytest.mark.parametrize("icurl_outputs", apic_post_boundary_switch_pre_boundary_case) -def test_apic_post_boundary_switch_pre_boundary_uses_crossing_formula(run_check, mock_icurl): +@pytest.mark.parametrize("icurl_outputs", apic_split_image_switch_pre_split_case) +def test_apic_split_image_switch_pre_split_uses_crossing_formula(run_check, mock_icurl): result = run_check( cversion=script.AciVersion("6.0(3a)"), sw_cversion=script.AciVersion("5.2(8h)"), @@ -154,6 +156,45 @@ def test_exact_target_downloaded_still_fails_on_insufficient_extraction_space(ru assert result.data == [["1", "101", "0.0", "2861.02"]] +downloaded_crossing_cases = [ + ( + { + partitions: [ + {"eqptcapacityFSPartition": {"attributes": {"dn": "topology/pod-1/node-101/sys/eqptcapacity/fspartition-bootflash", "avail": str(4 * 1024 ** 2), "used": "0"}}}, + ], + download_sts_615: maintUpgJob_node_101_downloaded, + firmware: firmware_crossing_2_3_3, + }, + script.FAIL_UF, + [["1", "101", "4096.0", "5120.0"]], + ), + ( + { + partitions: [ + {"eqptcapacityFSPartition": {"attributes": {"dn": "topology/pod-1/node-101/sys/eqptcapacity/fspartition-bootflash", "avail": str(5 * 1024 ** 2), "used": "0"}}}, + ], + download_sts_615: maintUpgJob_node_101_downloaded, + firmware: firmware_crossing_2_3_3, + }, + script.PASS, + [], + ), +] + + +@pytest.mark.parametrize("icurl_outputs, expected_result, expected_data", downloaded_crossing_cases) +def test_downloaded_crossing_uses_remaining_crossing_requirement( + run_check, mock_icurl, expected_result, expected_data +): + result = run_check( + sw_cversion=script.AciVersion("5.2(8h)"), + tversion=script.AciVersion("6.1(5e)"), + ) + + assert result.result == expected_result + assert result.data == expected_data + + def test_missing_target_version_takes_precedence(run_check): result = run_check(sw_cversion=None, tversion=None) @@ -287,8 +328,8 @@ def test_missing_target_version_takes_precedence(run_check): "6.1(5e)", script.MANUAL, ), - # node-101 (the only node that would otherwise fail) already fully downloaded - # the target image, so it's excluded from the check and the result is PASS. + # node-101 downloaded the target and has enough space for the remaining + # extraction-only requirement, so the result is PASS. ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), @@ -310,7 +351,8 @@ def test_missing_target_version_takes_precedence(run_check): "6.0(2h)", script.FAIL_UF, ), - # Every node has pre-downloaded the image, so all are skipped and the result is PASS. + # Every node downloaded the image and has enough space for the remaining + # extraction-only requirement, so the result is PASS. ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), @@ -357,8 +399,8 @@ def test_missing_target_version_takes_precedence(run_check): "6.1(5e)", script.FAIL_UF, ), - # Pre-downloading every node that would otherwise fail with the insufficient-space - # fixture leaves only the already-sufficient nodes, so the result is PASS. + # Every node that fails the full crossing requirement downloaded the 32-bit target + # and has enough space for the reduced remaining requirement, so the result is PASS. ( { partitions: read_data(dir, "eqptcapacityFSPartition.json"), From acaecc1f95e7790fcd387ae4d0a1ef156186ea14 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 15 Sep 2026 09:56:07 -0400 Subject: [PATCH 19/21] docs: clarify bootflash validation Summarize what the dynamic bootflash check validates and update the stale sample result message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/docs/index.md | 2 +- docs/docs/validations.md | 14 ++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/docs/docs/index.md b/docs/docs/index.md index 388b11f7..89e05c6e 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -105,7 +105,7 @@ You have chosen version "aci-apic-dk9.5.2.1d.bin" [Check 11/47] Switch Upgrade Group Guidelines... No upgrade groups found! MANUAL CHECK REQUIRED [Check 12/47] APIC Disk Space Usage (F1527, F1528, F1529 equipment-full)... PASS -[Check 13/47] Switch Node /bootflash usage... all below 50% PASS +[Check 13/47] Switch Node /bootflash usage... All nodes have sufficient bootflash space PASS [Check 14/47] Standby APIC Disk Space Usage... No standby APIC found N/A [Check 15/47] APIC SSD Health... PASS [Check 16/47] Switch SSD Health (F3073, F3074 equipment-flash-warning)... PASS diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 80486135..3926b4dd 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -651,19 +651,9 @@ To prevent this, check the `/bootflash` prior to an upgrade and take the necessa The pre-upgrade validation built into Cisco APIC upgrade workflow monitors the fault F1821, which can capture the high utilization of any partition. When this fault is present, we recommend that you resolve it prior to the upgrade even if the fault is not for bootflash. -The ACI Pre-Upgrade Validation script (this script) dynamically calculates the actual bootflash space required for the target upgrade, rather than relying on a fixed usage threshold, and compares it against each switch's available `/bootflash` space. Starting with 6.0(2a), switch releases use separate 32-bit and 64-bit images. The calculation therefore distinguishes between pre-split releases older than 6.0(2a) and split-image releases starting with 6.0(2a): +The ACI Pre-Upgrade Validation script checks whether each switch has enough available `/bootflash` space to download and extract the target switch image. The required space is calculated dynamically from the current and target switch releases, including upgrades that cross the 6.0(2) 32-bit/64-bit image boundary and targets that have already been downloaded. -* A pre-split target requires twice the 32-bit target image size. -* An upgrade from a pre-split release to a split-image release requires `2 × (32-bit target size + 64-bit target size − current image size)`. If the 32-bit target is not larger than the current image, the check instead uses twice the larger target image. -* An upgrade where both the current and target releases are in the split-image era requires twice the larger target image. - -Nodes that already downloaded the exact target version (`maintUpgJob.dnldStatus == downloaded` and `desiredVersion` matching target) are still evaluated because extraction and later upgrade stages require additional bootflash space. Since the downloaded image is already reflected in the current available-space value: - -* A pre-split target requires one 32-bit target image of remaining space. -* A boundary-crossing upgrade requires the full crossing requirement minus the downloaded 32-bit target image. -* A split-image-era upgrade requires the larger target image of remaining space. - -* If a required firmware image isn't found in the Firmware Repository, the check reports a manual review. +If a switch does not have the calculated amount of available space, the check reports an upgrade failure. If the required switch version, firmware image, or bootflash information is unavailable, the check reports that a manual review is required. ### APIC SSD Health From 8db9a751fa58fca2f76bee123e8834986fa62939 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 15 Sep 2026 10:08:45 -0400 Subject: [PATCH 20/21] docs: explain bootflash space check Describe the cleanup threshold, dynamic image-space calculation, downloaded-image handling, and validation outcomes without exposing implementation detail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/docs/validations.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 3926b4dd..68d2acf7 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -634,26 +634,26 @@ The script performs SSH into each standby Cisco APIC as `rescue-user`, then run ### Switch Node `/bootflash` usage -ACI switches mainly have two different faults about the filesystem usage of each partition: +ACI switches mainly have two different faults related to filesystem usage on each partition: -* **F1820**: A minor level fault for switch partition usage. This is raised when the utilization of the partition exceeds the minor threshold. +* **F1820**: A minor-level fault raised when partition utilization exceeds the minor threshold. -* **F1821**: A major level fault for switch partition usage. This is raised when the utilization of the partition exceeds the major threshold. +* **F1821**: A major-level fault raised when partition utilization exceeds the major threshold. !!! note - The threshold for minor and major depends on partitions. The critical one for upgrades is `/bootflash`. The threshold of bootflash is 80% for minor and 90% for major threshold. + Thresholds vary by partition. For `/bootflash`, the minor threshold is 80% utilization and the major threshold is 90%. -On top of this, there is a built-in behavior added to every switch node where it will take action to ensure that the `/bootflash` directory maintains 50% capacity. This is specifically to ensure that switch upgrades are able to successfully transfer and extract the switch image over during an upgrade. +ACI switches also include an internal cleanup process intended to maintain sufficient free `/bootflash` capacity for switch upgrades. When usage exceeds approximately 50%, the process can remove eligible files to make space for transferring and extracting switch images. -To do this, there is an internal script that is monitoring `/bootflash` usage and, if over 50% usage, it will start removing files to free up the filesystem. Given its aggressiveness, there are some corner case scenarios where this cleanup script could potentially trigger against the switch image it is intending to use, which can result in a switch upgrade booting a switch into the loader prompt given that the boot image was removed from `/bootflash`. +The fixed cleanup threshold does not cover every upgrade scenario. Larger target images, files that cannot be removed, and upgrades that cross the ACI 6.0(2) 32-bit/64-bit image boundary can require more free space than the cleanup process normally maintains. Insufficient space can prevent an image from being downloaded or extracted and may cause the switch upgrade to fail. -To prevent this, check the `/bootflash` prior to an upgrade and take the necessary steps to understand what is written there and why. Once understood, take the necessary steps to clear up unnecessary `/bootflash` files to ensure there is enough space to prevent the auto-cleanup corner case scenario. +The ACI Pre-Upgrade Validation script uses APIC API data to compare each switch's available `/bootflash` space with the space required for the target switch release. The requirement is calculated dynamically from the current and target switch images rather than from a fixed utilization percentage. -The pre-upgrade validation built into Cisco APIC upgrade workflow monitors the fault F1821, which can capture the high utilization of any partition. When this fault is present, we recommend that you resolve it prior to the upgrade even if the fault is not for bootflash. +The calculation generally reserves twice the applicable target image size. This accounts for space used by the downloaded image and additional space needed while the image is extracted. For an upgrade that crosses the 6.0(2) image boundary, the calculation accounts for both the 32-bit and 64-bit target images and the space recovered when the current image is removed. -The ACI Pre-Upgrade Validation script checks whether each switch has enough available `/bootflash` space to download and extract the target switch image. The required space is calculated dynamically from the current and target switch releases, including upgrades that cross the 6.0(2) 32-bit/64-bit image boundary and targets that have already been downloaded. +Switches that have already downloaded the target image are still checked because image extraction and later upgrade stages can require additional space. Because the downloaded image is already reflected in the switch's available-space value, the check evaluates only the remaining space required to complete the upgrade. -If a switch does not have the calculated amount of available space, the check reports an upgrade failure. If the required switch version, firmware image, or bootflash information is unavailable, the check reports that a manual review is required. +If a switch does not have enough available space, the check reports an upgrade failure and displays the available and required space. If the current switch version, target firmware image, or `/bootflash` information is unavailable, the check reports that a manual review is required. ### APIC SSD Health From 64876e9cb44d4c91ccfa1f07a1d9d4d7ed8460a8 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 15 Sep 2026 10:50:34 -0400 Subject: [PATCH 21/21] docs: clarify bootflash remediation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- aci-preupgrade-validation-script.py | 5 ++++- docs/docs/validations.md | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 21389d37..e8995f60 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2163,7 +2163,10 @@ def switch_bootflash_usage_check(sw_cversion, tversion, **kwargs): msg = '' headers = ["Pod-ID", "Node-ID", "Avail (MB)", "Required (MB)"] data = [] - recommended_action = "Insufficient free space to download and extract the target image! Contact Cisco TAC for Support" + recommended_action = ( + "Remove old, unused switch images to free bootflash space, then re-run this validation. " + "Contact Cisco TAC if sufficient space cannot be recovered." + ) doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#switch-node-bootflash-usage" if not tversion: diff --git a/docs/docs/validations.md b/docs/docs/validations.md index 68d2acf7..b680acf8 100644 --- a/docs/docs/validations.md +++ b/docs/docs/validations.md @@ -653,7 +653,7 @@ The calculation generally reserves twice the applicable target image size. This Switches that have already downloaded the target image are still checked because image extraction and later upgrade stages can require additional space. Because the downloaded image is already reflected in the switch's available-space value, the check evaluates only the remaining space required to complete the upgrade. -If a switch does not have enough available space, the check reports an upgrade failure and displays the available and required space. If the current switch version, target firmware image, or `/bootflash` information is unavailable, the check reports that a manual review is required. +If a switch does not have enough available space, the check reports an upgrade failure and displays the available and required space. Remove old, unused switch images to recover space, then run the validation again. Contact Cisco TAC if sufficient space cannot be recovered. If the current switch version, target firmware image, or `/bootflash` information is unavailable, the check reports that a manual review is required. ### APIC SSD Health