From e1e6827244c6a9b6bac6d9bf99f96a7b33f5f7a3 Mon Sep 17 00:00:00 2001 From: sr73318 Date: Thu, 24 Sep 2026 14:08:05 +0530 Subject: [PATCH 1/4] CSTACKEX-301: automation for cs volume resize --- test/integration/plugins/ontap/README.md | 12 +- test/integration/plugins/ontap/TEST_CASES.md | 46 ++-- .../iscsi/instance/test_vm_volume_attach.py | 240 ++++++++++++++++-- .../nfs3/instance/test_vm_volume_attach.py | 236 +++++++++++++++-- .../plugins/ontap/ontap_test_base.py | 161 ++++++++++-- 5 files changed, 619 insertions(+), 76 deletions(-) diff --git a/test/integration/plugins/ontap/README.md b/test/integration/plugins/ontap/README.md index 2adbf449af28..7b61a80766ea 100644 --- a/test/integration/plugins/ontap/README.md +++ b/test/integration/plugins/ontap/README.md @@ -43,7 +43,7 @@ for the deployment, health-gate, and artifact contracts. test/integration/plugins/ontap/ ├── ontap.cfg # Environment config (IPs, credentials, zone info) ├── ontap_test_base.py # Shared base class and ONTAP REST client -├── TEST_CASES.md # Full test case reference table (72 tests) +├── TEST_CASES.md # Full test case reference table (82 tests) ├── README.md # This file │ ├── nfs3/ @@ -54,7 +54,7 @@ test/integration/plugins/ontap/ │ ├── volume/ │ │ └── test_volume_lifecycle.py # Volume create/delete/negative-delete │ ├── instance/ -│ │ └── test_vm_volume_attach.py # Pool + volume + VM + attach/detach + template cache +│ │ └── test_vm_volume_attach.py # Pool + volume + VM + attach/resize/detach + template cache │ └── template/ │ └── test_template_cache_negative.py # Template-cache boundary cases │ @@ -66,7 +66,7 @@ test/integration/plugins/ontap/ ├── volume/ │ └── test_volume_lifecycle.py # LUN create/delete/negative-delete ├── instance/ - │ └── test_vm_volume_attach.py # Pool + LUN + VM + attach/LUN-map lifecycle + template cache + │ └── test_vm_volume_attach.py # Pool + LUN + VM + attach/resize/LUN-map lifecycle + template cache └── template/ └── test_template_cache_negative.py # Template-cache boundary cases ``` @@ -334,13 +334,13 @@ self.assertEqual(result.state, "Maintenance") | NFS3 Pool with Volumes | `nfs3/pool/test_pool_with_volumes.py` | 7 | Same + live volume present; negative delete guard | | NFS3 Zone-Scoped Pool | `nfs3/pool/test_zone_scoped_pool.py` | 4 | Zone scope — all hosts connected via `attachZone` | | NFS3 Volume Lifecycle | `nfs3/volume/test_volume_lifecycle.py` | 5 | Volume is metadata-only; FlexVol unchanged on delete | -| NFS3 VM + Volume Attach | `nfs3/instance/test_vm_volume_attach.py` | 10 | Full VM lifecycle with hot-plug/detach; ROOT on tagged pool seeds/reuses template cache, which survives VM delete | +| NFS3 VM + Volume Attach | `nfs3/instance/test_vm_volume_attach.py` | 14 | Full VM lifecycle with stopped-VM qcow2 grow; ROOT on tagged pool seeds/reuses template cache, which survives VM delete | | NFS3 Template Cache Negative | `nfs3/template/test_template_cache_negative.py` | 3 | Tag mismatch; undersized pool; out-of-band cache delete | | iSCSI Pool Lifecycle | `iscsi/pool/test_pool_lifecycle.py` | 8 | Create, disable, enable, maintenance, delete + igroups | | iSCSI Pool with Volumes | `iscsi/pool/test_pool_with_volumes.py` | 7 | Same + live LUN present; negative delete guard | | iSCSI Zone-Scoped Pool | `iscsi/pool/test_zone_scoped_pool.py` | 4 | Zone scope | | iSCSI Volume Lifecycle | `iscsi/volume/test_volume_lifecycle.py` | 5 | LUN created per CS volume; LUN removed on delete | -| iSCSI VM + Volume Attach | `iscsi/instance/test_vm_volume_attach.py` | 10 | Full VM lifecycle; LUN-maps on VM start/stop/detach; ROOT on tagged pool seeds/reuses `cs_tmpl_*` LUN cache | +| iSCSI VM + Volume Attach | `iscsi/instance/test_vm_volume_attach.py` | 14 | Full VM lifecycle; stopped-VM LUN grow and LUN-map checks; ROOT on tagged pool seeds/reuses `cs_tmpl_*` LUN cache | | iSCSI Template Cache Negative | `iscsi/template/test_template_cache_negative.py` | 3 | Tag mismatch; undersized pool; out-of-band cache delete | For the goal, dependencies, and exact success criteria of every individual test, see [TEST_CASES.md](TEST_CASES.md). @@ -356,7 +356,7 @@ For the goal, dependencies, and exact success criteria of every individual test, | `Lost connection to MySQL` | MySQL not accepting remote connections | Enable remote MySQL access (see Prerequisites §3) | | `sh: python: command not found` (repeated) | Marvin internal call — harmless on macOS | Ignore; Marvin Init still succeeds | | Pool state never reaches `Maintenance` | KVM agent not responding | Check `cloudstack-agent` on KVM host; verify host is connected in CloudStack UI | -| iSCSI `test_07` error 530 | KVM guest does not ACK SCSI hot-unplug | Known environment limitation — see TEST_CASES.md Suite 10 note | +| iSCSI `test_10` error 530 | KVM guest does not ACK SCSI hot-unplug | Known environment limitation — see TEST_CASES.md Suite 10 note | | ONTAP REST `401 Unauthorized` | Wrong credentials in `ontap.cfg` | Verify `username`/`password` under `ontap` section | | `ISCSI protocol is not enabled on SVM` / many pool tests EXCEPTION then FAIL | SVM iSCSI service off or no `data_iscsi` LIF | Enable iSCSI on the SVM and assign `default-data-iscsi` (or `default-data-blocks`) to at least one data LIF; `run_tests.sh` now fails this check before Marvin starts | | `No ready KVM user template available` | Template still downloading | Re-run `setup_zone` (step 12 waits for template readiness); or wait in CloudStack UI | diff --git a/test/integration/plugins/ontap/TEST_CASES.md b/test/integration/plugins/ontap/TEST_CASES.md index d3f2b1be58d1..7607ecd1998e 100644 --- a/test/integration/plugins/ontap/TEST_CASES.md +++ b/test/integration/plugins/ontap/TEST_CASES.md @@ -19,7 +19,7 @@ # ONTAP Integration Test Cases -Complete reference for all 72 test cases across 12 test suites. +Complete reference for all 82 test cases across 12 test suites. Each suite is sequential — tests must run in numbered order; each step builds on state created by the previous step. --- @@ -114,7 +114,7 @@ Each suite is sequential — tests must run in numbered order; each step builds **File:** `nfs3/instance/test_vm_volume_attach.py` **Class:** `TestOntapVMVolumeAttach` **Tag:** `vm_volume_workflow` -**Total:** 10 tests | **Scope:** end-to-end — NFS3 pool, data volume, running VM (ROOT on the ONTAP pool via a tagged compute offering), primary template cache seed / reuse / survive VM delete, attach/detach lifecycle +**Total:** 14 tests | **Scope:** end-to-end — NFS3 pool, data volume resize, running VM (ROOT on the ONTAP pool via a tagged compute offering), primary template cache seed / reuse / survive VM delete, attach/detach lifecycle | # | Test method | Goal | Depends on | CloudStack success criteria | ONTAP success criteria | Type | |---|-------------|------|------------|-----------------------------|------------------------|------| @@ -123,11 +123,15 @@ Each suite is sequential — tests must run in numbered order; each step builds | 03 | `test_03_deploy_vm` | Deploy a VM with the tagged SO — ROOT on ONTAP; seeds template cache | test_02 (`pool`, `volume`) | `vm.state == "Running"`; ROOT `storageid` = pool; `template_spool_ref` Ready/DOWNLOADED | Cache file present at spool `install_path` | positive | | 03a | `test_03a_deploy_second_vm_reuses_template_cache` | Deploy VM-2 — reuses cache | test_03 | VM-2 Running; ROOT on pool; still exactly one `template_spool_ref` | Same cache file (no second cache) | positive | | 03b | `test_03b_expunge_second_vm_template_cache_survives` | Expunge VM-2 — cache must remain (lazy GC) | test_03a | spool_ref still Ready | Cache file still present | positive | -| 04 | `test_04_attach_volume_to_vm` | Attach the ONTAP data volume to the running VM (hot-plug) | test_03 (`vm`, `volume`) | `volume.virtualmachineid == vm.id`; `attachVolume` job succeeds | FlexVol `online`; after attach, a data file matching volume UUID present in FlexVol (`list_files_in_volume`) | positive | -| 05 | `test_05_stop_vm_export_retained` | Stop the running VM with volume attached | test_04 | `vm.state == "Stopped"` | FlexVol still `online`; NFS export policy still present | positive | -| 06 | `test_06_start_vm_volume_accessible` | Start the stopped VM | test_05 | `vm.state == "Running"` | FlexVol still `online` | positive | -| 07 | `test_07_detach_volume_from_vm` | Hot-detach the ONTAP volume from the running VM (TDS Detach NFS3) | test_06 (`vm`, `volume`) | `volume.virtualmachineid` cleared; `volume.state == "Ready"` | FlexVol still `online`; data file **still present** (NFS3: file persists until `deleteVolume`, not on detach) | positive | -| 08 | `test_08_destroy_vm_and_cleanup` | Destroy VM (expunge), delete volume, enter maintenance, force-delete pool | test_07 | VM no longer listed; spool_ref still Ready after VM expunge; volume no longer listed; pool no longer listed | Cache file present after VM expunge; FlexVol deleted; export policy deleted | cleanup | +| 04 | `test_04_attach_volume_to_vm` | Attach the ONTAP data volume to the running VM (hot-plug) | test_03b (`vm`, `volume`) | `volume.virtualmachineid == vm.id`; `attachVolume` job succeeds | FlexVol `online`; after attach, a data file matching volume UUID present in FlexVol (`list_files_in_volume`) | positive | +| 05 | `test_05_resize_running_vm_rejected` | Attempt to grow an attached managed volume while its VM is running | test_04 | `resizeVolume` is rejected; volume size remains unchanged | qcow2 virtual size remains unchanged | negative | +| 06 | `test_06_stop_vm_export_retained` | Stop the running VM with volume attached | test_05 | `vm.state == "Stopped"` | FlexVol still `online`; NFS export policy still present | positive | +| 07 | `test_07_grow_stopped_vm_volume` | Grow the attached volume while the VM is stopped | test_06 | Volume is `Ready` at the requested byte size | `qemu-img info` reports the requested virtual size; ONTAP file remains present | positive | +| 08 | `test_08_shrink_rejected` | Attempt to shrink the grown qcow2 volume with `shrinkok=true` | test_07 | Resize is rejected; grown size remains unchanged | qcow2 virtual size remains unchanged | negative | +| 09 | `test_09_start_vm_volume_accessible` | Start the stopped VM | test_08 | `vm.state == "Running"`; KVM data-disk capacity equals target | FlexVol still `online` | positive | +| 10 | `test_10_detach_volume_from_vm` | Hot-detach the ONTAP volume from the running VM (TDS Detach NFS3) | test_09 (`vm`, `volume`) | `volume.virtualmachineid` cleared; `volume.state == "Ready"` | FlexVol still `online`; data file **still present** (NFS3: file persists until `deleteVolume`, not on detach) | positive | +| 11 | `test_11_grow_detached_volume` | Grow the detached, materialized data volume | test_10 | API and DB size equal target | qcow2 virtual size equals target | positive | +| 12 | `test_12_destroy_vm_and_cleanup` | Destroy VM (expunge), delete volume, enter maintenance, force-delete pool | test_11 | VM no longer listed; spool_ref still Ready after VM expunge; volume no longer listed; pool no longer listed | Cache file present after VM expunge; FlexVol deleted; export policy deleted | cleanup | --- @@ -208,22 +212,26 @@ Each suite is sequential — tests must run in numbered order; each step builds **File:** `iscsi/instance/test_vm_volume_attach.py` **Class:** `TestOntapVMVolumeAttachISCSI` **Tag:** `iscsi_vm_workflow` -**Total:** 10 tests | **Scope:** end-to-end — iSCSI pool, data volume (LUN), running VM (ROOT on the ONTAP pool via a tagged compute offering), `cs_tmpl_` LUN cache seed / reuse / survive VM delete, attach/stop/start/detach lifecycle +**Total:** 14 tests | **Scope:** end-to-end — iSCSI pool, data volume resize (LUN), running VM (ROOT on the ONTAP pool via a tagged compute offering), `cs_tmpl_` LUN cache seed / reuse / survive VM delete, attach/stop/start/detach lifecycle | # | Test method | Goal | Depends on | CloudStack success criteria | ONTAP success criteria | Type | |---|-------------|------|------------|-----------------------------|------------------------|------| | 01 | `test_01_create_iscsi_pool` | Create iSCSI ONTAP primary storage pool tagged `-tmpl-cache` | setUpClass (tagged SO) | `pool.state == "Up"`, `pool.type == "OntapiSCSI"` | FlexVol `online`; igroup per cluster host with host IQN | positive | -| 02 | `test_02_create_ontap_data_volume` | Allocate a CloudStack data volume (creates a LUN in the FlexVol) | test_01 (`pool`) | Volume non-None | ≥1 LUN in FlexVol | positive | +| 02 | `test_02_create_ontap_data_volume` | Allocate and grow a detached data volume | test_01 (`pool`) | API and DB size equal target | LUN exists and `space.size` equals target | positive | | 03 | `test_03_deploy_vm` | Deploy VM with the tagged SO — ROOT on ONTAP; seeds `cs_tmpl_*`; verify 0 data-volume LUN-maps before attach | test_02 (`volume`) | `vm.state == "Running"`; ROOT on pool; spool_ref Ready (`local_path` = LUN uuid) | Exactly one `/vol//cs_tmpl_` LUN; 0 data-volume LUN-maps | positive | | 03a | `test_03a_deploy_second_vm_reuses_template_cache` | Deploy VM-2 — reuse cache | test_03 | VM-2 Running; ROOT on pool; still one spool_ref | Still one `cs_tmpl_*`; non-cache LUN count +1 | positive | | 03b | `test_03b_expunge_second_vm_template_cache_survives` | Expunge VM-2 — cache LUN remains | test_03a | spool_ref still Ready | VM-2 ROOT LUN gone (non-cache count back to baseline); `cs_tmpl_*` still present | positive | -| 04 | `test_04_attach_volume_to_vm` | Hot-attach the ONTAP iSCSI volume to the running VM — a LUN-map is created (TDS SN 27) | test_03 (`vm`, `volume`) | `volume.virtualmachineid == vm.id` | ≥1 LUN-map linking the LUN to the host's igroup | positive | -| 05 | `test_05_stop_vm_lun_unmapped` | Stop VM — LUN-maps must be removed (TDS VM Stop iSCSI) | test_04 | `vm.state == "Stopped"` | 0 LUN-maps; LUN itself **still present** in FlexVol | positive | -| 06 | `test_06_start_vm_lun_remapped` | Start VM — LUN-maps must be re-created (TDS VM Start iSCSI) | test_05 | `vm.state == "Running"` | ≥1 LUN-map re-created | positive | -| 07 | `test_07_detach_volume_from_vm` | Hot-detach the iSCSI volume from the running VM (TDS Detach iSCSI) | test_06 (`vm`, `volume`) | `volume.virtualmachineid` cleared | 0 LUN-maps; LUN still in FlexVol | positive ⚠️ | -| 08 | `test_08_destroy_vm_and_cleanup` | Destroy VM (expunge), delete volume, enter maintenance, delete pool | test_07 | VM gone; spool_ref still Ready after VM expunge; volume gone; pool gone | `cs_tmpl_*` present after VM expunge; FlexVol deleted; all LUNs and igroups deleted | cleanup | - -> ⚠️ **test_07 known status:** iSCSI hot-detach from a running VM relies on the KVM guest acknowledging the SCSI device removal. On this environment the guest does not acknowledge in time, causing CloudStack error 530. This is a KVM-host-level or guest-template limitation, not a test code defect. All other 61 tests pass. +| 04 | `test_04_attach_volume_to_vm` | Hot-attach the ONTAP iSCSI volume to the running VM — a LUN-map is created (TDS SN 27) | test_03b (`vm`, `volume`) | `volume.virtualmachineid == vm.id` | ≥1 LUN-map linking the LUN to the host's igroup | positive | +| 05 | `test_05_resize_running_vm_rejected` | Attempt to grow the attached managed LUN while its VM is running | test_04 | `resizeVolume` is rejected; volume size remains unchanged | LUN `space.size` remains unchanged | negative | +| 06 | `test_06_stop_vm_lun_unmapped` | Stop VM — LUN-maps must be removed (TDS VM Stop iSCSI) | test_05 | `vm.state == "Stopped"` | 0 LUN-maps; LUN itself **still present** in FlexVol | positive | +| 07 | `test_07_grow_stopped_vm_volume` | Grow the attached volume while the VM is stopped | test_06 | Volume is `Ready` at the requested byte size | LUN `space.size` equals the requested size; LUN remains unmapped | positive | +| 08 | `test_08_shrink_rejected` | Attempt to shrink the grown LUN with `shrinkok=true` | test_07 | Resize is rejected; grown size remains unchanged | LUN `space.size` remains unchanged | negative | +| 09 | `test_09_start_vm_lun_remapped` | Start VM — LUN-maps must be re-created (TDS VM Start iSCSI) | test_08 | `vm.state == "Running"`; KVM data-disk capacity equals target | ≥1 LUN-map re-created | positive | +| 10 | `test_10_detach_volume_from_vm` | Hot-detach the iSCSI volume from the running VM (TDS Detach iSCSI) | test_09 (`vm`, `volume`) | `volume.virtualmachineid` cleared | 0 LUN-maps; LUN still in FlexVol | positive ⚠️ | +| 11 | `test_11_grow_detached_volume` | Grow the detached, materialized data volume | test_10 | API and DB size equal target | LUN `space.size` equals target | positive | +| 12 | `test_12_destroy_vm_and_cleanup` | Destroy VM (expunge), delete volume, enter maintenance, force-delete pool | test_11 | VM gone; spool_ref still Ready after VM expunge; volume gone; pool gone | `cs_tmpl_*` present after VM expunge; FlexVol deleted; all LUNs and igroups deleted | cleanup | + +> ⚠️ **test_10 known status:** iSCSI hot-detach from a running VM relies on the KVM guest acknowledging the SCSI device removal. On this environment the guest does not acknowledge in time, causing CloudStack error 530. This is a KVM-host-level or guest-template limitation, not a test code defect. --- @@ -265,12 +273,12 @@ Each suite is sequential — tests must run in numbered order; each step builds | NFS3 Pool with Volumes | NFS3 | Cluster | 7 | ✅ | | NFS3 Zone-Scoped Pool | NFS3 | Zone | 4 | ✅ | | NFS3 Volume Lifecycle | NFS3 | Cluster | 5 | ✅ | -| NFS3 VM + Volume Attach | NFS3 | Cluster | 10 | 🆕 +2 template cache | +| NFS3 VM + Volume Attach | NFS3 | Cluster | 14 | ⏳ resize cases pending lab validation | | NFS3 Template Cache Negative | NFS3 | Cluster | 3 | 🆕 | | iSCSI Pool Lifecycle | iSCSI | Cluster | 8 | ✅ | | iSCSI Pool with Volumes | iSCSI | Cluster | 7 | ✅ | | iSCSI Zone-Scoped Pool | iSCSI | Zone | 4 | ✅ | | iSCSI Volume Lifecycle | iSCSI | Cluster | 5 | ✅ | -| iSCSI VM + Volume Attach | iSCSI | Cluster | 10 | ⚠️ 7/8 + 🆕 2 template cache | +| iSCSI VM + Volume Attach | iSCSI | Cluster | 14 | ⏳ resize cases pending lab validation; detach limitation | | iSCSI Template Cache Negative | iSCSI | Cluster | 3 | 🆕 | -| **Total** | | | **72** | | +| **Total** | | | **82** | **New resize cases pending lab validation** | diff --git a/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py b/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py index 2e247c5e2f87..f9a6327a79e2 100644 --- a/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py +++ b/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py @@ -34,15 +34,15 @@ - startVirtualMachine → LUN-map is re-created - detachVolume → LUN-map is removed -Tests must run in order (test_01, test_02, test_03, test_03a, test_03b, -test_04 ... test_08). Each step builds on the shared state established by the +Tests are numbered test_01 ... test_12, plus test_03a and test_03b, and must +run in that order. Each step builds on the shared state established by the previous step. The VM is deployed with a compute offering whose storage tags match the pool, so its ROOT volume lands on the ONTAP pool and exercises the primary template cache (``cs_tmpl_`` LUN: seed / reuse / survive VM delete) -alongside the data-volume workflow. LUN-map assertions are filtered to the -data volume's LUN, so the ROOT and cache LUNs in the same FlexVol do not +alongside the data-volume resize workflow. LUN-map assertions are filtered to +the data volume's LUN, so the ROOT and cache LUNs in the same FlexVol do not affect them. Workflow: @@ -52,10 +52,14 @@ 03a Deploy a second VM — reuses the cache LUN (one more ROOT clone LUN only) 03b Expunge the second VM — its ROOT LUN is removed; cache LUN survives 04 Attach iSCSI data volume to running VM (LUN-map created) - 05 Stop VM — LUN-map for attached volume is removed from ONTAP - 06 Start VM — LUN-map is re-created on ONTAP - 07 Detach data volume from running VM — LUN-map removed - 08 Destroy VM (cache LUN still survives), delete data volume, delete pool + 05 Verify resize is rejected while the attached VM is running + 06 Stop VM — LUN-map for attached volume is removed from ONTAP + 07 Grow the attached volume and ONTAP LUN while the VM is stopped + 08 Verify shrinking the ONTAP LUN is rejected without changing its size + 09 Start VM — LUN-map is re-created on ONTAP + 10 Detach data volume from running VM — LUN-map removed + 11 Grow the detached data volume + 12 Destroy VM (cache LUN still survives), delete data volume, delete pool Prerequisites: - CloudStack management server with the NetApp ONTAP plugin deployed @@ -215,6 +219,8 @@ class TestOntapVMVolumeAttachISCSI(OntapTestBase): service_offering_id = None network_id = None _created_network_id = None # network created by this suite for Advanced zones + initial_volume_size = None + target_volume_size = None _vol_name_prefix = "OntapISCSIVM" @@ -320,7 +326,7 @@ def setUpClass(cls): def tearDownClass(cls): """ Safety-net cleanup: destroy VMs if still alive, delete the guest - network created for Advanced zones (if not already deleted by test_08), + network created for Advanced zones (if not already deleted by test_12), then delegate pool/volume/account cleanup to the base class. """ for vm in (cls.vm2, cls.vm): @@ -343,7 +349,7 @@ def tearDownClass(cls): cls.vm = None # Delete the guest network created for this account in Advanced zones. - # test_08 deletes it on the happy path; this is the fallback for + # test_12 deletes it on the happy path; this is the fallback for # mid-suite failures. if cls._created_network_id is not None: try: @@ -456,6 +462,17 @@ def _data_luns(self, volume=None): self.svm_name, self.__class__.pool.name) if lun.get("name") == lun_name] + def _data_lun_size(self): + """Return the ONTAP data LUN size in bytes.""" + lun_name = self._data_lun_name() + lun = self.ontap.get_lun(self.svm_name, lun_name) + self.assertIsNotNone( + lun, "ONTAP LUN '%s' was not found" % lun_name) + size = int(lun.get("space", {}).get("size", 0)) + self.assertGreater( + size, 0, "ONTAP LUN '%s' did not report space.size" % lun_name) + return size + # ================================================================== # Test steps # ================================================================== @@ -498,10 +515,12 @@ def test_01_create_iscsi_pool(self): @attr(tags=["iscsi_vm_workflow"], required_hardware=True) def test_02_create_ontap_data_volume(self): """ - Allocate a CloudStack data volume on the iSCSI ONTAP pool. + Allocate and grow a detached CloudStack data volume on the iSCSI + ONTAP pool. Verifies: - createVolume returns a volume object - ONTAP: at least one LUN is created inside the pool's FlexVol + - detached resize updates CloudStack API, DB, and LUN space.size """ self.assertIsNotNone(self.__class__.pool, "Pool absent — test_01 must pass first") @@ -518,6 +537,19 @@ def test_02_create_ontap_data_volume(self): % (self._data_lun_name(), self.__class__.pool.name) ) + volume = self._poll_volume_size(vol.id, int(vol.size)) + detached_target = ( + (int(volume.size) // (1024 ** 3)) + 1 + ) * (1024 ** 3) + self._resize_volume(vol.id, detached_target // (1024 ** 3)) + self._poll_volume_size(vol.id, detached_target) + self.assertEqual( + self._get_db_volume_size(vol.id), detached_target, + "DB size does not match detached iSCSI grow target") + self.assertEqual( + self._data_lun_size(), detached_target, + "ONTAP LUN size does not match detached grow target") + # ------------------------------------------------------------------ # Step 03 — Deploy a VM # ------------------------------------------------------------------ @@ -685,12 +717,61 @@ def test_04_attach_volume_to_vm(self): "LUN is not accessible to the VM's host" ) + initial_size = int(result.size) + self.__class__.initial_volume_size = initial_size + self.__class__.target_volume_size = ( + (initial_size // (1024 ** 3)) + 1 + ) * (1024 ** 3) + self.assertEqual( + self._data_lun_size(), initial_size, + "Initial ONTAP LUN size should match the CloudStack volume size") + + # ------------------------------------------------------------------ + # Step 05 — Resize attached volume while VM is running must fail + # ------------------------------------------------------------------ + + @attr(tags=["iscsi_vm_workflow"], required_hardware=True) + def test_05_resize_running_vm_rejected(self): + """ + Attempt to grow the attached iSCSI volume while its managed KVM VM is + running. + Verifies: + - CloudStack rejects the resize + - CloudStack volume and ONTAP LUN sizes remain unchanged + """ + if self.__class__.vm is None: + self.skipTest("VM not deployed — test_03 was skipped") + self.assertIsNotNone(self.__class__.volume, + "Volume absent — test_02 must pass first") + + target_gib = self.__class__.target_volume_size // (1024 ** 3) + with self.assertRaises(Exception) as context: + self._resize_volume(self.__class__.volume.id, target_gib) + self.assertIn( + "not in the Stopped state", str(context.exception), + "Resize failed for an unexpected reason") + + volume = self._poll_volume_size( + self.__class__.volume.id, self.__class__.initial_volume_size) + self.assertIsNotNone( + volume, "CloudStack volume size changed after rejected resize") + + # DB: size must remain unchanged after a rejected resize + db_size = self._get_db_volume_size(self.__class__.volume.id) + self.assertEqual( + db_size, self.__class__.initial_volume_size, + "cloud DB volumes.size changed after rejected running-VM resize") + + self.assertEqual( + self._data_lun_size(), self.__class__.initial_volume_size, + "ONTAP LUN size changed after rejected running-VM resize") + # ------------------------------------------------------------------ - # Step 05 — Stop VM — LUN-maps should be removed + # Step 06 — Stop VM — LUN-maps should be removed # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_05_stop_vm_lun_unmapped(self): + def test_06_stop_vm_lun_unmapped(self): """ Stop the running VM while the iSCSI data volume is still attached. Covers TDS VM Stop (iSCSI): 'Luns for the volumes under this VM @@ -704,7 +785,15 @@ def test_05_stop_vm_lun_unmapped(self): cmd = stopVirtualMachineAPI.stopVirtualMachineCmd() cmd.id = self.__class__.vm.id - self.apiClient.stopVirtualMachine(cmd) + try: + self.apiClient.stopVirtualMachine(cmd) + except Exception as exc: + # Marvin can lose the management API connection while polling an + # already-submitted async stop job. The VM state is authoritative, + # so continue polling before treating the stop as failed. + logger.warning( + "stopVirtualMachine polling failed; checking VM state: %s", + exc) result = self._poll_vm_state(self.__class__.vm.id, "Stopped", timeout=300) @@ -729,11 +818,99 @@ def test_05_stop_vm_lun_unmapped(self): ) # ------------------------------------------------------------------ - # Step 06 — Start VM — LUN-maps should be re-created + # Step 07 — Grow attached volume while VM is stopped + # ------------------------------------------------------------------ + + @attr(tags=["iscsi_vm_workflow"], required_hardware=True) + def test_07_grow_stopped_vm_volume(self): + """ + Grow the attached iSCSI volume while its managed KVM VM is stopped. + Verifies: + - CloudStack reports the target size and Ready state + - ONTAP reports the target LUN space.size + - The LUN remains unmapped while the VM is stopped + """ + if self.__class__.vm is None: + self.skipTest("VM not deployed — test_03 was skipped") + self.assertIsNotNone(self.__class__.volume, + "Volume absent — test_02 must pass first") + + target_size = self.__class__.target_volume_size + self._resize_volume( + self.__class__.volume.id, target_size // (1024 ** 3)) + + volume = self._poll_volume_size( + self.__class__.volume.id, target_size) + self.assertIsNotNone( + volume, "CloudStack did not report the grown volume size") + + # DB: cloud.volumes.size must be updated by the driver's volumeDao call + db_size = self._get_db_volume_size(self.__class__.volume.id) + self.assertEqual( + db_size, target_size, + "cloud DB volumes.size (%s) does not match target (%s) after grow" + % (db_size, target_size)) + + # ONTAP: LUN space.size must reflect the new allocation + self.assertEqual( + self._data_lun_size(), target_size, + "ONTAP LUN space.size does not match the requested size") + self.assertEqual( + len(self._lun_maps()), 0, + "The data LUN should remain unmapped while the VM is stopped") + + # ------------------------------------------------------------------ + # Step 08 — Shrink must fail + # ------------------------------------------------------------------ + + @attr(tags=["iscsi_vm_workflow"], required_hardware=True) + def test_08_shrink_rejected(self): + """ + Attempt to shrink the grown iSCSI volume back to its initial size. + Verifies: + - The ONTAP driver rejects shrink even with shrinkok=true + - CloudStack and ONTAP LUN sizes remain at the grown value + """ + if self.__class__.vm is None: + self.skipTest("VM not deployed — test_03 was skipped") + self.assertIsNotNone(self.__class__.volume, + "Volume absent — test_02 must pass first") + self.assertEqual( + self._get_db_volume_size(self.__class__.volume.id), + self.__class__.target_volume_size, + "Volume was not grown to the target size — test_07 must pass " + "before the shrink check") + + initial_gib = self.__class__.initial_volume_size // (1024 ** 3) + with self.assertRaises(Exception) as context: + self._resize_volume( + self.__class__.volume.id, initial_gib, shrink_ok=True) + self.assertIn( + "does not support shrinking", str(context.exception), + "Shrink failed for an unexpected reason") + + volume = self._poll_volume_size( + self.__class__.volume.id, self.__class__.target_volume_size) + self.assertIsNotNone( + volume, "CloudStack volume size changed after rejected shrink") + + # DB: size must remain at grown value after rejected shrink + db_size = self._get_db_volume_size(self.__class__.volume.id) + self.assertEqual( + db_size, self.__class__.target_volume_size, + "cloud DB volumes.size did not remain at the grown target after " + "the rejected iSCSI shrink") + + self.assertEqual( + self._data_lun_size(), self.__class__.target_volume_size, + "ONTAP LUN size changed after rejected shrink") + + # ------------------------------------------------------------------ + # Step 09 — Start VM — LUN-maps should be re-created # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_06_start_vm_lun_remapped(self): + def test_09_start_vm_lun_remapped(self): """ Start the stopped VM. Covers TDS VM Start (iSCSI): 'luns should be re-mapped again to @@ -764,11 +941,11 @@ def test_06_start_vm_lun_remapped(self): ) # ------------------------------------------------------------------ - # Step 07 — Detach volume from running VM + # Step 10 — Detach volume from running VM # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_07_detach_volume_from_vm(self): + def test_10_detach_volume_from_vm(self): """ Detach the iSCSI data volume from the running VM. Verifies: @@ -827,11 +1004,34 @@ def test_07_detach_volume_from_vm(self): ) # ------------------------------------------------------------------ - # Step 08 — Destroy VM and clean up pool + # Step 11 — Grow detached data volume + # ------------------------------------------------------------------ + + @attr(tags=["iscsi_vm_workflow"], required_hardware=True) + def test_11_grow_detached_volume(self): + """Grow the materialized LUN after it has been detached.""" + self.assertIsNotNone(self.__class__.volume, + "Volume absent — test_02 must pass first") + detached_target = self.__class__.target_volume_size + (1024 ** 3) + self._resize_volume( + self.__class__.volume.id, detached_target // (1024 ** 3)) + + self._poll_volume_size(self.__class__.volume.id, detached_target) + self.assertEqual( + self._get_db_volume_size(self.__class__.volume.id), + detached_target, + "DB size does not match detached iSCSI grow target") + self.assertEqual( + self._data_lun_size(), detached_target, + "ONTAP LUN size does not match detached grow target") + self.__class__.target_volume_size = detached_target + + # ------------------------------------------------------------------ + # Step 12 — Destroy VM and clean up pool # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_08_destroy_vm_and_cleanup(self): + def test_12_destroy_vm_and_cleanup(self): """ Destroy the VM (with expunge), delete the data volume, force-delete the ONTAP pool, and delete the guest network created for this suite. diff --git a/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py b/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py index dbd6b210ce85..df7bf02117db 100644 --- a/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py +++ b/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py @@ -19,14 +19,14 @@ Sequential workflow integration tests for NetApp ONTAP data volume lifecycle with a running virtual machine. -Tests must run in order (test_01, test_02, test_03, test_03a, test_03b, -test_04 ... test_08). Each step builds on the shared state established by the +Tests are numbered test_01 ... test_12, plus test_03a and test_03b, and must +run in that order. Each step builds on the shared state established by the previous step. The VM is deployed with a compute offering whose storage tags match the pool, so its ROOT volume lands on the ONTAP pool and exercises the primary template cache (seed / reuse / survive VM delete) alongside the data-volume -workflow. +resize workflow. Workflow: 01 Create NFS3 primary storage pool on ONTAP (tagged -tmpl-cache) @@ -36,10 +36,14 @@ 03a Deploy a second VM — reuses the cache (still one template_spool_ref) 03b Expunge the second VM — cache and template_spool_ref survive 04 Attach the ONTAP data volume to the running VM - 05 Stop the VM — export policy stays; volume remains attached in CS - 06 Start the VM — VM Running; volume still attached; FlexVol online - 07 Detach the ONTAP data volume from the VM - 08 Destroy VM (cache still survives); delete ONTAP volume; enter + 05 Verify resize is rejected while the attached VM is running + 06 Stop the VM — export policy stays; volume remains attached in CS + 07 Grow the attached volume and its qcow2 file while the VM is stopped + 08 Verify shrinking the volume is rejected without changing its size + 09 Start the VM — VM Running; volume still attached; FlexVol online + 10 Detach the ONTAP data volume from the VM + 11 Grow the detached data volume + 12 Destroy VM (cache still survives); delete ONTAP volume; enter maintenance; force-delete pool Prerequisites: @@ -178,6 +182,9 @@ class TestOntapVMVolumeAttach(OntapTestBase): service_offering_id = None network_id = None # None for Basic zones _created_network_id = None # network created by this suite for Advanced zones + initial_volume_size = None + target_volume_size = None + volume_path = None _vol_name_prefix = "OntapVMVol" @@ -629,12 +636,70 @@ def test_04_attach_volume_to_vm(self): "after attach; files present: %s" % (vol.id, pool.name, files) ) + volumes = self.apiClient.listVolumes(_list_vols_cmd(vol.id)) or [] + self.assertTrue(volumes, "Attached volume is not listed in CloudStack") + initial_size = int(volumes[0].size) + self.__class__.initial_volume_size = initial_size + self.__class__.target_volume_size = ( + (initial_size // (1024 ** 3)) + 1 + ) * (1024 ** 3) + self.__class__.volume_path = vol_file + + backend_size = self._get_qcow2_virtual_size(pool.id, vol_file) + self.assertEqual( + backend_size, initial_size, + "Initial qcow2 virtual size should match the CloudStack volume size" + ) + + # ------------------------------------------------------------------ + # Step 05 - Resize attached volume while VM is running must fail + # ------------------------------------------------------------------ + + @attr(tags=["vm_volume_workflow"], required_hardware=True) + def test_05_resize_running_vm_rejected(self): + """ + Attempt to grow the attached volume while its managed KVM VM is + running. + Verifies: + - CloudStack rejects the resize + - CloudStack volume size is unchanged + - qcow2 virtual size is unchanged + """ + if self.__class__.vm is None: + self.skipTest("VM not deployed — test_03 was skipped") + self.assertIsNotNone(self.__class__.volume, + "Volume absent — test_02 must pass first") + + target_gib = self.__class__.target_volume_size // (1024 ** 3) + with self.assertRaises(Exception) as context: + self._resize_volume(self.__class__.volume.id, target_gib) + self.assertIn( + "not in the Stopped state", str(context.exception), + "Resize failed for an unexpected reason") + + volume = self._poll_volume_size( + self.__class__.volume.id, self.__class__.initial_volume_size) + self.assertIsNotNone( + volume, "CloudStack volume size changed after rejected resize") + + # DB: size must remain unchanged after a rejected resize + db_size = self._get_db_volume_size(self.__class__.volume.id) + self.assertEqual( + db_size, self.__class__.initial_volume_size, + "cloud DB volumes.size changed after rejected running-VM resize") + + backend_size = self._get_qcow2_virtual_size( + self.__class__.pool.id, self.__class__.volume_path) + self.assertEqual( + backend_size, self.__class__.initial_volume_size, + "qcow2 virtual size changed after rejected running-VM resize") + # ------------------------------------------------------------------ - # Step 05 - Stop VM — export policy must be retained + # Step 06 - Stop VM — export policy must be retained # ------------------------------------------------------------------ @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_05_stop_vm_export_retained(self): + def test_06_stop_vm_export_retained(self): """ Stop the running VM while the NFS3 data volume is still attached. Unlike iSCSI (where LUN-maps are removed on VM stop), NFS3 export @@ -655,7 +720,15 @@ def test_05_stop_vm_export_retained(self): cmd = stopVirtualMachineAPI.stopVirtualMachineCmd() cmd.id = vm.id - self.apiClient.stopVirtualMachine(cmd) + try: + self.apiClient.stopVirtualMachine(cmd) + except Exception as exc: + # Marvin can lose the management API connection while polling an + # already-submitted async stop job. The VM state is authoritative, + # so continue polling before treating the stop as failed. + logger.warning( + "stopVirtualMachine polling failed; checking VM state: %s", + exc) result = self._poll_vm_state(vm.id, "Stopped", timeout=300) self.assertEqual( @@ -688,11 +761,109 @@ def test_05_stop_vm_export_retained(self): ) # ------------------------------------------------------------------ - # Step 06 - Start VM — volume accessible; FlexVol online + # Step 07 - Grow attached volume while VM is stopped + # ------------------------------------------------------------------ + + @attr(tags=["vm_volume_workflow"], required_hardware=True) + def test_07_grow_stopped_vm_volume(self): + """ + Grow the attached NFS3 volume while its managed KVM VM is stopped. + Verifies: + - CloudStack reports the target size and Ready state + - KVM reports the target qcow2 virtual size + - The backing file remains present in the ONTAP FlexVol + """ + if self.__class__.vm is None: + self.skipTest("VM not deployed — test_03 was skipped") + self.assertIsNotNone(self.__class__.volume, + "Volume absent — test_02 must pass first") + + target_size = self.__class__.target_volume_size + self._resize_volume( + self.__class__.volume.id, target_size // (1024 ** 3)) + + volume = self._poll_volume_size( + self.__class__.volume.id, target_size) + self.assertIsNotNone( + volume, "CloudStack did not report the grown volume size") + + # KVM: qcow2 virtual size must reflect the new allocation + backend_size = self._get_qcow2_virtual_size( + self.__class__.pool.id, self.__class__.volume_path) + self.assertEqual( + backend_size, target_size, + "KVM qcow2 virtual size does not match the requested size") + + # DB: cloud.volumes.size must be updated by the driver's volumeDao call + db_size = self._get_db_volume_size(self.__class__.volume.id) + self.assertEqual( + db_size, target_size, + "cloud DB volumes.size (%s) does not match target (%s) after grow" + % (db_size, target_size)) + + # ONTAP REST: backing file must still be present in the FlexVol. + # Note: ONTAP's file-API returns bytes_used (actual disk blocks), NOT + # the qcow2 virtual size, so the host-side qemu-img check above is the + # correct NFS3 backend verification for virtual size. + files = self.ontap.list_files_in_volume(self.__class__.pool.name) + self.assertIn( + self.__class__.volume_path, files, + "NFS3 backing file disappeared from ONTAP after resize") + + # ------------------------------------------------------------------ + # Step 08 - Shrink must fail + # ------------------------------------------------------------------ + + @attr(tags=["vm_volume_workflow"], required_hardware=True) + def test_08_shrink_rejected(self): + """ + Attempt to shrink the grown NFS3 volume back to its initial size. + Verifies: + - CloudStack rejects qcow2 shrink even with shrinkok=true + - CloudStack and qcow2 sizes remain at the grown value + """ + if self.__class__.vm is None: + self.skipTest("VM not deployed — test_03 was skipped") + self.assertIsNotNone(self.__class__.volume, + "Volume absent — test_02 must pass first") + self.assertEqual( + self._get_db_volume_size(self.__class__.volume.id), + self.__class__.target_volume_size, + "Volume was not grown to the target size — test_07 must pass " + "before the shrink check") + + initial_gib = self.__class__.initial_volume_size // (1024 ** 3) + with self.assertRaises(Exception) as context: + self._resize_volume( + self.__class__.volume.id, initial_gib, shrink_ok=True) + self.assertIn( + "Unable to shrink volumes of type QCOW2", str(context.exception), + "Shrink failed for an unexpected reason") + + volume = self._poll_volume_size( + self.__class__.volume.id, self.__class__.target_volume_size) + self.assertIsNotNone( + volume, "CloudStack volume size changed after rejected shrink") + + # DB: size must remain at grown value after rejected shrink + db_size = self._get_db_volume_size(self.__class__.volume.id) + self.assertEqual( + db_size, self.__class__.target_volume_size, + "cloud DB volumes.size did not remain at the grown target after " + "the rejected NFS3 shrink") + + backend_size = self._get_qcow2_virtual_size( + self.__class__.pool.id, self.__class__.volume_path) + self.assertEqual( + backend_size, self.__class__.target_volume_size, + "qcow2 virtual size changed after rejected shrink") + + # ------------------------------------------------------------------ + # Step 09 - Start VM — volume accessible; FlexVol online # ------------------------------------------------------------------ @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_06_start_vm_volume_accessible(self): + def test_09_start_vm_volume_accessible(self): """ Start the stopped VM. Verifies: @@ -743,12 +914,20 @@ def test_06_start_vm_volume_accessible(self): "got virtualmachineid=%s" % (vm.id, vol_vmid) ) + instance_name = getattr(result, "instancename", None) + self.assertIsNotNone( + instance_name, "Running VM response did not include instancename") + self.assertEqual( + self._get_vm_data_disk_capacity(instance_name), + self.__class__.target_volume_size, + "KVM data-disk capacity does not reflect the NFS3 resize") + # ------------------------------------------------------------------ - # Step 07 - Detach ONTAP data volume from the VM + # Step 10 - Detach ONTAP data volume from the VM # ------------------------------------------------------------------ @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_07_detach_volume_from_vm(self): + def test_10_detach_volume_from_vm(self): """ Detach the ONTAP data volume from the running VM. Verifies: @@ -831,11 +1010,36 @@ def test_07_detach_volume_from_vm(self): ) # ------------------------------------------------------------------ - # Step 08 - Destroy VM, delete volume, delete pool + # Step 11 - Grow detached data volume + # ------------------------------------------------------------------ + + @attr(tags=["vm_volume_workflow"], required_hardware=True) + def test_11_grow_detached_volume(self): + """Grow the materialized qcow2 after it has been detached.""" + self.assertIsNotNone(self.__class__.volume, + "Volume absent — test_02 must pass first") + detached_target = self.__class__.target_volume_size + (1024 ** 3) + self._resize_volume( + self.__class__.volume.id, detached_target // (1024 ** 3)) + + self._poll_volume_size(self.__class__.volume.id, detached_target) + self.assertEqual( + self._get_db_volume_size(self.__class__.volume.id), + detached_target, + "DB size does not match detached NFS3 grow target") + self.assertEqual( + self._get_qcow2_virtual_size( + self.__class__.pool.id, self.__class__.volume_path), + detached_target, + "qcow2 virtual size does not match detached grow target") + self.__class__.target_volume_size = detached_target + + # ------------------------------------------------------------------ + # Step 12 - Destroy VM, delete volume, delete pool # ------------------------------------------------------------------ @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_08_destroy_vm_and_cleanup(self): + def test_12_destroy_vm_and_cleanup(self): """ Destroy the VM, delete the ONTAP data volume, enter maintenance, then force-delete the pool. diff --git a/test/integration/plugins/ontap/ontap_test_base.py b/test/integration/plugins/ontap/ontap_test_base.py index 17bec2c1251c..129dc9f011cf 100644 --- a/test/integration/plugins/ontap/ontap_test_base.py +++ b/test/integration/plugins/ontap/ontap_test_base.py @@ -28,9 +28,11 @@ template cache) """ +import json import logging import random import requests +import shlex import sys import time import urllib3 @@ -45,6 +47,7 @@ listDiskOfferings as listDiskOfferingsAPI, listVirtualMachines as listVirtualMachinesAPI, listVolumes as listVolumesAPI, + resizeVolume as resizeVolumeAPI, updateStoragePool as updateStoragePoolAPI, ) from marvin.cloudstackAPI import listHosts as listHostsAPI @@ -258,7 +261,7 @@ def get_lun(self, svm_name, lun_path): """Return the ONTAP LUN record for the given full path, or None.""" data = self._get("/storage/luns", params={"svm.name": svm_name, "name": lun_path, - "fields": "name,uuid,enabled,status"}) + "fields": "name,uuid,enabled,status,space.size"}) records = data.get("records", []) return records[0] if records else None @@ -267,7 +270,7 @@ def list_luns_in_volume(self, svm_name, vol_name): prefix = "/vol/%s/" % vol_name data = self._get("/storage/luns", params={"svm.name": svm_name, - "fields": "name,uuid,enabled,status"}) + "fields": "name,uuid,enabled,status,space.size"}) return [r for r in data.get("records", []) if r.get("name", "").startswith(prefix)] @@ -526,19 +529,21 @@ def _setup_cloudstack_resources(cls, config, account_testdata): list_hosts_cmd.type = "Routing" cls.cluster_hosts = cls.apiClient.listHosts(list_hosts_cmd) or [] - list_do_cmd = listDiskOfferingsAPI.listDiskOfferingsCmd() - list_do_cmd.domainid = cls.domain.id - offerings = cls.apiClient.listDiskOfferings(list_do_cmd) - if offerings: - cls.disk_offering_id = offerings[0].id - else: - # No disk offerings exist yet — create a minimal one for tests - do = DiskOffering.create( - cls.apiClient, - {"name": "ontap-test-do", "displaytext": "ONTAP test disk offering", "disksize": 2}, - ) - cls._cleanup.append(do) - cls.disk_offering_id = do.id + # Use a deterministic, non-strict 2 GiB offering. Selecting the first + # offering in the environment can choose a fixed-size or very large + # offering that cannot exercise the resize API on the small test pool. + offering_name = "ontap-test-do-%d" % random.randint(0, 99999) + do = DiskOffering.create( + cls.apiClient, + { + "name": offering_name, + "displaytext": "ONTAP test disk offering", + "disksize": 2, + "storagetype": "shared", + }, + ) + cls._cleanup.append(do) + cls.disk_offering_id = do.id # Parse KVM host SSH credentials from zones/pods/clusters/hosts config. # Used by _cleanup_kvm_storage_pool_mounts to unmount stale NFS pools. @@ -894,6 +899,132 @@ def _create_volume(self, pool_id): cmd.domainid = self.domain.id return self.apiClient.createVolume(cmd) + def _resize_volume(self, volume_id, size_gib, shrink_ok=False): + """Resize a volume to ``size_gib`` through the CloudStack API.""" + cmd = resizeVolumeAPI.resizeVolumeCmd() + cmd.id = volume_id + cmd.size = size_gib + if shrink_ok: + cmd.shrinkok = True + return self.apiClient.resizeVolume(cmd) + + def _poll_volume_size(self, volume_id, expected_bytes, timeout=180, + interval=5): + """Poll until a volume is Ready with the expected size in bytes.""" + deadline = time.time() + timeout + last_size = None + last_state = "not listed" + while time.time() < deadline: + cmd = listVolumesAPI.listVolumesCmd() + cmd.id = volume_id + cmd.listall = True + volumes = self.apiClient.listVolumes(cmd) or [] + if volumes: + size = int(getattr(volumes[0], "size", 0) or 0) + state = getattr(volumes[0], "state", "") + last_size = size + last_state = state + if size == expected_bytes and state == "Ready": + return volumes[0] + time.sleep(interval) + self.fail( + "Volume %s did not reach Ready with size %s within %ds " + "(last size=%s, state=%s)" + % (volume_id, expected_bytes, timeout, last_size, last_state) + ) + + def _get_qcow2_virtual_size(self, pool_uuid, volume_path): + """Read a qcow2 virtual size from the KVM host mounting the NFS pool.""" + pool_arg = shlex.quote(str(pool_uuid)) + volume_arg = shlex.quote(str(volume_path)) + command = ( + "path=$(virsh vol-path --pool {pool} {volume} 2>/dev/null || true); " + "if [ -z \"$path\" ]; then path=/mnt/{pool}/{volume}; fi; " + "qemu_img=$(command -v cloud-qemu-img || command -v qemu-img); " + "test -n \"$qemu_img\" || " + "{{ echo 'qemu-img executable not found' >&2; exit 1; }}; " + "\"$qemu_img\" info -U --output=json \"$path\"" + ).format(pool=pool_arg, volume=volume_arg) + + errors = [] + for creds in self.kvm_hosts_ssh_creds: + try: + ssh = SshClient( + creds["host"], 22, creds["user"], creds["password"], + retries=3, delay=3, timeout=15.0, + ) + output = "\n".join(ssh.execute(command)) + json_start = output.find("{") + if json_start < 0: + raise ValueError(output) + info, _ = json.JSONDecoder().raw_decode(output[json_start:]) + return int(info["virtual-size"]) + except Exception as exc: + errors.append("%s: %s" % (creds.get("host", "?"), exc)) + + raise RuntimeError( + "Could not read qcow2 virtual size from any KVM host: %s" + % "; ".join(errors) + ) + + def _get_vm_data_disk_capacity(self, instance_name): + """Return the libvirt capacity of the VM's first data disk in bytes. + + The ONTAP VM workflow attaches exactly one data disk. The first libvirt + disk is therefore the root disk and the second is the resized data + disk, independent of whether its source is NFS or iSCSI multipath. + """ + instance_arg = shlex.quote(str(instance_name)) + command = ( + "target=$(virsh domblklist {vm} --details | " + "awk '$2 == \"disk\" {{ count++; if (count == 2) " + "{{ print $3; exit }} }}'); " + "test -n \"$target\" || " + "{{ echo 'data disk target not found' >&2; exit 1; }}; " + "virsh domblkinfo {vm} \"$target\" | " + "awk '$1 == \"Capacity:\" {{ print $2; exit }}'" + ).format(vm=instance_arg) + + errors = [] + for creds in self.kvm_hosts_ssh_creds: + try: + ssh = SshClient( + creds["host"], 22, creds["user"], creds["password"], + retries=3, delay=3, timeout=15.0, + ) + output = "\n".join(ssh.execute(command)).strip() + if not output: + raise ValueError("virsh returned no data-disk capacity") + return int(output.splitlines()[-1].strip()) + except Exception as exc: + errors.append("%s: %s" % (creds.get("host", "?"), exc)) + + raise RuntimeError( + "Could not read VM data-disk capacity from any KVM host: %s" + % "; ".join(errors) + ) + + def _get_db_volume_size(self, volume_uuid): + """Query the cloud DB directly for the size (bytes) stored in the + ``volumes`` table for *volume_uuid*. + + Uses the per-suite ``dbConnection`` set in ``setUpClass`` (consistent + with the pattern in all other ONTAP test suites). + + Returns the integer size, or None if the row is not found. + This validates that the ONTAP driver's ``volumeDao.update()`` call + persisted the new size to the database — independently of the + CloudStack API layer. + """ + db = self.__class__.dbConnection + rows = db.execute( + "SELECT size FROM volumes WHERE uuid = %s AND removed IS NULL", + params=(volume_uuid,), + ) + if not rows: + return None + return int(rows[0][0]) + def _delete_pool(self, pool_id, forced=False): """Issue deleteStoragePool for the given pool id.""" cmd = deleteStoragePoolAPI.deleteStoragePoolCmd() From cc697d4ac3af763eda8bf7019dc592789cf67578 Mon Sep 17 00:00:00 2001 From: sr73318 Date: Mon, 28 Sep 2026 12:13:52 +0530 Subject: [PATCH 2/4] CSTACKEX-301: resolving comments --- .../ontap/iscsi/instance/test_vm_volume_attach.py | 10 +--------- .../ontap/nfs3/instance/test_vm_volume_attach.py | 10 +--------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py b/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py index f9a6327a79e2..808884457dbf 100644 --- a/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py +++ b/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py @@ -785,15 +785,7 @@ def test_06_stop_vm_lun_unmapped(self): cmd = stopVirtualMachineAPI.stopVirtualMachineCmd() cmd.id = self.__class__.vm.id - try: - self.apiClient.stopVirtualMachine(cmd) - except Exception as exc: - # Marvin can lose the management API connection while polling an - # already-submitted async stop job. The VM state is authoritative, - # so continue polling before treating the stop as failed. - logger.warning( - "stopVirtualMachine polling failed; checking VM state: %s", - exc) + self.apiClient.stopVirtualMachine(cmd) result = self._poll_vm_state(self.__class__.vm.id, "Stopped", timeout=300) diff --git a/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py b/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py index df7bf02117db..d1641e1db9a8 100644 --- a/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py +++ b/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py @@ -720,15 +720,7 @@ def test_06_stop_vm_export_retained(self): cmd = stopVirtualMachineAPI.stopVirtualMachineCmd() cmd.id = vm.id - try: - self.apiClient.stopVirtualMachine(cmd) - except Exception as exc: - # Marvin can lose the management API connection while polling an - # already-submitted async stop job. The VM state is authoritative, - # so continue polling before treating the stop as failed. - logger.warning( - "stopVirtualMachine polling failed; checking VM state: %s", - exc) + self.apiClient.stopVirtualMachine(cmd) result = self._poll_vm_state(vm.id, "Stopped", timeout=300) self.assertEqual( From 6725306cd8137f26e3ac30e6ab68634c3c8a748d Mon Sep 17 00:00:00 2001 From: sr73318 Date: Mon, 12 Oct 2026 11:18:53 +0530 Subject: [PATCH 3/4] CSTACKEX-301: address volume resize review comments Move KVM host SSH helpers out of the shared test base, match the iSCSI shrink assertion to the driver message, and check the ONTAP backing file when a running-VM resize is rejected. Co-authored-by: Cursor --- test/integration/plugins/ontap/TEST_CASES.md | 2 +- .../plugins/ontap/helpers/kvm_host.py | 137 ++++++++++++++++++ .../iscsi/instance/test_vm_volume_attach.py | 2 +- .../nfs3/instance/test_vm_volume_attach.py | 8 +- .../plugins/ontap/ontap_test_base.py | 116 ++------------- 5 files changed, 155 insertions(+), 110 deletions(-) create mode 100644 test/integration/plugins/ontap/helpers/kvm_host.py diff --git a/test/integration/plugins/ontap/TEST_CASES.md b/test/integration/plugins/ontap/TEST_CASES.md index 7607ecd1998e..3e7e77e74602 100644 --- a/test/integration/plugins/ontap/TEST_CASES.md +++ b/test/integration/plugins/ontap/TEST_CASES.md @@ -124,7 +124,7 @@ Each suite is sequential — tests must run in numbered order; each step builds | 03a | `test_03a_deploy_second_vm_reuses_template_cache` | Deploy VM-2 — reuses cache | test_03 | VM-2 Running; ROOT on pool; still exactly one `template_spool_ref` | Same cache file (no second cache) | positive | | 03b | `test_03b_expunge_second_vm_template_cache_survives` | Expunge VM-2 — cache must remain (lazy GC) | test_03a | spool_ref still Ready | Cache file still present | positive | | 04 | `test_04_attach_volume_to_vm` | Attach the ONTAP data volume to the running VM (hot-plug) | test_03b (`vm`, `volume`) | `volume.virtualmachineid == vm.id`; `attachVolume` job succeeds | FlexVol `online`; after attach, a data file matching volume UUID present in FlexVol (`list_files_in_volume`) | positive | -| 05 | `test_05_resize_running_vm_rejected` | Attempt to grow an attached managed volume while its VM is running | test_04 | `resizeVolume` is rejected; volume size remains unchanged | qcow2 virtual size remains unchanged | negative | +| 05 | `test_05_resize_running_vm_rejected` | Attempt to grow an attached managed volume while its VM is running | test_04 | `resizeVolume` is rejected; volume size remains unchanged | ONTAP backing file still present; qcow2 virtual size remains unchanged | negative | | 06 | `test_06_stop_vm_export_retained` | Stop the running VM with volume attached | test_05 | `vm.state == "Stopped"` | FlexVol still `online`; NFS export policy still present | positive | | 07 | `test_07_grow_stopped_vm_volume` | Grow the attached volume while the VM is stopped | test_06 | Volume is `Ready` at the requested byte size | `qemu-img info` reports the requested virtual size; ONTAP file remains present | positive | | 08 | `test_08_shrink_rejected` | Attempt to shrink the grown qcow2 volume with `shrinkok=true` | test_07 | Resize is rejected; grown size remains unchanged | qcow2 virtual size remains unchanged | negative | diff --git a/test/integration/plugins/ontap/helpers/kvm_host.py b/test/integration/plugins/ontap/helpers/kvm_host.py new file mode 100644 index 000000000000..de3b90dcc11e --- /dev/null +++ b/test/integration/plugins/ontap/helpers/kvm_host.py @@ -0,0 +1,137 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""KVM host SSH helpers for ONTAP Marvin tests. + +Host-side checks (libvirt, qemu-img, NFS mount cleanup) live here so +``ontap_test_base`` stays focused on CloudStack and ONTAP REST helpers. +""" + +import json +import logging +import shlex + +from marvin.sshClient import SshClient + +logger = logging.getLogger("kvm_host") + + +def cleanup_kvm_storage_pool_mounts(kvm_hosts_ssh_creds, pool_uuid): + """Unmount and undefine the libvirt pool for ``pool_uuid`` on each host. + + Call this before the ONTAP FlexVol is deleted so the unmount finishes + while the NFS export is still reachable. Stale NFS mounts can make + KVMHAMonitor reboot the host. + """ + for creds in kvm_hosts_ssh_creds: + host_ip = creds["host"] + try: + ssh = SshClient( + host_ip, 22, + creds["user"], creds["password"], + retries=3, delay=3, timeout=15.0, + ) + for cmd in [ + "umount -f -l /mnt/{u} 2>/dev/null; true".format(u=pool_uuid), + "virsh pool-destroy {u} 2>/dev/null; true".format(u=pool_uuid), + "virsh pool-undefine {u} 2>/dev/null; true".format(u=pool_uuid), + ]: + try: + ssh.execute(cmd) + except Exception as cmd_ex: + logger.warning( + "cleanup_kvm_storage_pool_mounts: cmd '%s' " + "failed on %s: %s" % (cmd, host_ip, cmd_ex) + ) + except Exception as ex: + logger.warning( + "cleanup_kvm_storage_pool_mounts: SSH to %s failed: %s" + % (host_ip, ex) + ) + + +def get_qcow2_virtual_size(kvm_hosts_ssh_creds, pool_uuid, volume_path): + """Read a qcow2 virtual size from a KVM host mounting the NFS pool.""" + pool_arg = shlex.quote(str(pool_uuid)) + volume_arg = shlex.quote(str(volume_path)) + command = ( + "path=$(virsh vol-path --pool {pool} {volume} 2>/dev/null || true); " + "if [ -z \"$path\" ]; then path=/mnt/{pool}/{volume}; fi; " + "qemu_img=$(command -v cloud-qemu-img || command -v qemu-img); " + "test -n \"$qemu_img\" || " + "{{ echo 'qemu-img executable not found' >&2; exit 1; }}; " + "\"$qemu_img\" info -U --output=json \"$path\"" + ).format(pool=pool_arg, volume=volume_arg) + + errors = [] + for creds in kvm_hosts_ssh_creds: + try: + ssh = SshClient( + creds["host"], 22, creds["user"], creds["password"], + retries=3, delay=3, timeout=15.0, + ) + output = "\n".join(ssh.execute(command)) + json_start = output.find("{") + if json_start < 0: + raise ValueError(output) + info, _ = json.JSONDecoder().raw_decode(output[json_start:]) + return int(info["virtual-size"]) + except Exception as exc: + errors.append("%s: %s" % (creds.get("host", "?"), exc)) + + raise RuntimeError( + "Could not read qcow2 virtual size from any KVM host: %s" + % "; ".join(errors) + ) + + +def get_vm_data_disk_capacity(kvm_hosts_ssh_creds, instance_name): + """Return the libvirt capacity of the VM's first data disk in bytes. + + The ONTAP VM workflow attaches exactly one data disk. The first libvirt + disk is the root disk and the second is the resized data disk, whether + its source is NFS or iSCSI multipath. + """ + instance_arg = shlex.quote(str(instance_name)) + command = ( + "target=$(virsh domblklist {vm} --details | " + "awk '$2 == \"disk\" {{ count++; if (count == 2) " + "{{ print $3; exit }} }}'); " + "test -n \"$target\" || " + "{{ echo 'data disk target not found' >&2; exit 1; }}; " + "virsh domblkinfo {vm} \"$target\" | " + "awk '$1 == \"Capacity:\" {{ print $2; exit }}'" + ).format(vm=instance_arg) + + errors = [] + for creds in kvm_hosts_ssh_creds: + try: + ssh = SshClient( + creds["host"], 22, creds["user"], creds["password"], + retries=3, delay=3, timeout=15.0, + ) + output = "\n".join(ssh.execute(command)).strip() + if not output: + raise ValueError("virsh returned no data-disk capacity") + return int(output.splitlines()[-1].strip()) + except Exception as exc: + errors.append("%s: %s" % (creds.get("host", "?"), exc)) + + raise RuntimeError( + "Could not read VM data-disk capacity from any KVM host: %s" + % "; ".join(errors) + ) diff --git a/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py b/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py index 808884457dbf..5fd2ae60e6a6 100644 --- a/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py +++ b/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py @@ -878,7 +878,7 @@ def test_08_shrink_rejected(self): self._resize_volume( self.__class__.volume.id, initial_gib, shrink_ok=True) self.assertIn( - "does not support shrinking", str(context.exception), + "Unable to shrink volume", str(context.exception), "Shrink failed for an unexpected reason") volume = self._poll_volume_size( diff --git a/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py b/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py index d1641e1db9a8..a8d1d6ae7415 100644 --- a/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py +++ b/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py @@ -661,8 +661,9 @@ def test_05_resize_running_vm_rejected(self): Attempt to grow the attached volume while its managed KVM VM is running. Verifies: - - CloudStack rejects the resize + - CloudStack rejects the resize while the VM is running - CloudStack volume size is unchanged + - The ONTAP backing file is still present in the FlexVol - qcow2 virtual size is unchanged """ if self.__class__.vm is None: @@ -688,6 +689,11 @@ def test_05_resize_running_vm_rejected(self): db_size, self.__class__.initial_volume_size, "cloud DB volumes.size changed after rejected running-VM resize") + files = self.ontap.list_files_in_volume(self.__class__.pool.name) + self.assertIn( + self.__class__.volume_path, files, + "ONTAP backing file disappeared after the rejected running-VM resize") + backend_size = self._get_qcow2_virtual_size( self.__class__.pool.id, self.__class__.volume_path) self.assertEqual( diff --git a/test/integration/plugins/ontap/ontap_test_base.py b/test/integration/plugins/ontap/ontap_test_base.py index 129dc9f011cf..2858bd7e25aa 100644 --- a/test/integration/plugins/ontap/ontap_test_base.py +++ b/test/integration/plugins/ontap/ontap_test_base.py @@ -28,11 +28,9 @@ template cache) """ -import json import logging import random import requests -import shlex import sys import time import urllib3 @@ -54,10 +52,10 @@ from marvin.cloudstackTestCase import cloudstackTestCase from marvin.jsonHelper import jsonDump from marvin.lib.base import Account, DiskOffering -from marvin.sshClient import SshClient from marvin.lib.common import get_domain, get_zone, list_clusters, list_storage_pools from marvin.lib.utils import cleanup_resources +from helpers import kvm_host from helpers import template_cache_util as tcu urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -572,44 +570,9 @@ def _setup_cloudstack_resources(cls, config, account_testdata): @classmethod def _cleanup_kvm_storage_pool_mounts(cls, pool_uuid): - """ - SSH to each KVM host and unmount the NFS storage pool mount for - *pool_uuid*, then destroy and undefine the libvirt storage pool. - - Must be called BEFORE the ONTAP FlexVol is deleted (i.e., before - deleteStoragePool) so that the unmount completes while the NFS - export is still reachable. Prevents stale NFS mounts from - triggering KVMHAMonitor heartbeat failures that reboot the host - via ``echo b > /proc/sysrq-trigger``. - """ - for creds in cls.kvm_hosts_ssh_creds: - host_ip = creds["host"] - try: - ssh = SshClient( - host_ip, 22, - creds["user"], creds["password"], - retries=3, delay=3, timeout=15.0, - ) - for cmd in [ - "umount -f -l /mnt/{u} 2>/dev/null; true".format( - u=pool_uuid), - "virsh pool-destroy {u} 2>/dev/null; true".format( - u=pool_uuid), - "virsh pool-undefine {u} 2>/dev/null; true".format( - u=pool_uuid), - ]: - try: - ssh.execute(cmd) - except Exception as cmd_ex: - logger.warning( - "_cleanup_kvm_storage_pool_mounts: cmd '%s' " - "failed on %s: %s" % (cmd, host_ip, cmd_ex) - ) - except Exception as ex: - logger.warning( - "_cleanup_kvm_storage_pool_mounts: SSH to %s failed: %s" - % (host_ip, ex) - ) + """Unmount and undefine the libvirt pool on each KVM host.""" + kvm_host.cleanup_kvm_storage_pool_mounts( + cls.kvm_hosts_ssh_creds, pool_uuid) # ---- shared teardown ----------------------------------------------- @@ -935,74 +898,13 @@ def _poll_volume_size(self, volume_id, expected_bytes, timeout=180, def _get_qcow2_virtual_size(self, pool_uuid, volume_path): """Read a qcow2 virtual size from the KVM host mounting the NFS pool.""" - pool_arg = shlex.quote(str(pool_uuid)) - volume_arg = shlex.quote(str(volume_path)) - command = ( - "path=$(virsh vol-path --pool {pool} {volume} 2>/dev/null || true); " - "if [ -z \"$path\" ]; then path=/mnt/{pool}/{volume}; fi; " - "qemu_img=$(command -v cloud-qemu-img || command -v qemu-img); " - "test -n \"$qemu_img\" || " - "{{ echo 'qemu-img executable not found' >&2; exit 1; }}; " - "\"$qemu_img\" info -U --output=json \"$path\"" - ).format(pool=pool_arg, volume=volume_arg) - - errors = [] - for creds in self.kvm_hosts_ssh_creds: - try: - ssh = SshClient( - creds["host"], 22, creds["user"], creds["password"], - retries=3, delay=3, timeout=15.0, - ) - output = "\n".join(ssh.execute(command)) - json_start = output.find("{") - if json_start < 0: - raise ValueError(output) - info, _ = json.JSONDecoder().raw_decode(output[json_start:]) - return int(info["virtual-size"]) - except Exception as exc: - errors.append("%s: %s" % (creds.get("host", "?"), exc)) - - raise RuntimeError( - "Could not read qcow2 virtual size from any KVM host: %s" - % "; ".join(errors) - ) + return kvm_host.get_qcow2_virtual_size( + self.kvm_hosts_ssh_creds, pool_uuid, volume_path) def _get_vm_data_disk_capacity(self, instance_name): - """Return the libvirt capacity of the VM's first data disk in bytes. - - The ONTAP VM workflow attaches exactly one data disk. The first libvirt - disk is therefore the root disk and the second is the resized data - disk, independent of whether its source is NFS or iSCSI multipath. - """ - instance_arg = shlex.quote(str(instance_name)) - command = ( - "target=$(virsh domblklist {vm} --details | " - "awk '$2 == \"disk\" {{ count++; if (count == 2) " - "{{ print $3; exit }} }}'); " - "test -n \"$target\" || " - "{{ echo 'data disk target not found' >&2; exit 1; }}; " - "virsh domblkinfo {vm} \"$target\" | " - "awk '$1 == \"Capacity:\" {{ print $2; exit }}'" - ).format(vm=instance_arg) - - errors = [] - for creds in self.kvm_hosts_ssh_creds: - try: - ssh = SshClient( - creds["host"], 22, creds["user"], creds["password"], - retries=3, delay=3, timeout=15.0, - ) - output = "\n".join(ssh.execute(command)).strip() - if not output: - raise ValueError("virsh returned no data-disk capacity") - return int(output.splitlines()[-1].strip()) - except Exception as exc: - errors.append("%s: %s" % (creds.get("host", "?"), exc)) - - raise RuntimeError( - "Could not read VM data-disk capacity from any KVM host: %s" - % "; ".join(errors) - ) + """Return the libvirt capacity of the VM's first data disk in bytes.""" + return kvm_host.get_vm_data_disk_capacity( + self.kvm_hosts_ssh_creds, instance_name) def _get_db_volume_size(self, volume_uuid): """Query the cloud DB directly for the size (bytes) stored in the From 434a0d7ad1ec1d9c13014b2804a1f35b86a6fc93 Mon Sep 17 00:00:00 2001 From: sr73318 Date: Mon, 12 Oct 2026 11:55:22 +0530 Subject: [PATCH 4/4] CSTACKEX-301: resolving comments --- test/integration/plugins/ontap/README.md | 8 +- test/integration/plugins/ontap/TEST_CASES.md | 44 ++++----- .../iscsi/instance/test_vm_volume_attach.py | 91 +++++------------- .../nfs3/instance/test_vm_volume_attach.py | 96 +++++-------------- 4 files changed, 73 insertions(+), 166 deletions(-) diff --git a/test/integration/plugins/ontap/README.md b/test/integration/plugins/ontap/README.md index 7b61a80766ea..9254e5d1d630 100644 --- a/test/integration/plugins/ontap/README.md +++ b/test/integration/plugins/ontap/README.md @@ -43,7 +43,7 @@ for the deployment, health-gate, and artifact contracts. test/integration/plugins/ontap/ ├── ontap.cfg # Environment config (IPs, credentials, zone info) ├── ontap_test_base.py # Shared base class and ONTAP REST client -├── TEST_CASES.md # Full test case reference table (82 tests) +├── TEST_CASES.md # Full test case reference table (80 tests) ├── README.md # This file │ ├── nfs3/ @@ -334,13 +334,13 @@ self.assertEqual(result.state, "Maintenance") | NFS3 Pool with Volumes | `nfs3/pool/test_pool_with_volumes.py` | 7 | Same + live volume present; negative delete guard | | NFS3 Zone-Scoped Pool | `nfs3/pool/test_zone_scoped_pool.py` | 4 | Zone scope — all hosts connected via `attachZone` | | NFS3 Volume Lifecycle | `nfs3/volume/test_volume_lifecycle.py` | 5 | Volume is metadata-only; FlexVol unchanged on delete | -| NFS3 VM + Volume Attach | `nfs3/instance/test_vm_volume_attach.py` | 14 | Full VM lifecycle with stopped-VM qcow2 grow; ROOT on tagged pool seeds/reuses template cache, which survives VM delete | +| NFS3 VM + Volume Attach | `nfs3/instance/test_vm_volume_attach.py` | 13 | Full VM lifecycle with stopped-VM qcow2 grow; ROOT on tagged pool seeds/reuses template cache, which survives VM delete | | NFS3 Template Cache Negative | `nfs3/template/test_template_cache_negative.py` | 3 | Tag mismatch; undersized pool; out-of-band cache delete | | iSCSI Pool Lifecycle | `iscsi/pool/test_pool_lifecycle.py` | 8 | Create, disable, enable, maintenance, delete + igroups | | iSCSI Pool with Volumes | `iscsi/pool/test_pool_with_volumes.py` | 7 | Same + live LUN present; negative delete guard | | iSCSI Zone-Scoped Pool | `iscsi/pool/test_zone_scoped_pool.py` | 4 | Zone scope | | iSCSI Volume Lifecycle | `iscsi/volume/test_volume_lifecycle.py` | 5 | LUN created per CS volume; LUN removed on delete | -| iSCSI VM + Volume Attach | `iscsi/instance/test_vm_volume_attach.py` | 14 | Full VM lifecycle; stopped-VM LUN grow and LUN-map checks; ROOT on tagged pool seeds/reuses `cs_tmpl_*` LUN cache | +| iSCSI VM + Volume Attach | `iscsi/instance/test_vm_volume_attach.py` | 13 | Full VM lifecycle; stopped-VM LUN grow and LUN-map checks; ROOT on tagged pool seeds/reuses `cs_tmpl_*` LUN cache | | iSCSI Template Cache Negative | `iscsi/template/test_template_cache_negative.py` | 3 | Tag mismatch; undersized pool; out-of-band cache delete | For the goal, dependencies, and exact success criteria of every individual test, see [TEST_CASES.md](TEST_CASES.md). @@ -356,7 +356,7 @@ For the goal, dependencies, and exact success criteria of every individual test, | `Lost connection to MySQL` | MySQL not accepting remote connections | Enable remote MySQL access (see Prerequisites §3) | | `sh: python: command not found` (repeated) | Marvin internal call — harmless on macOS | Ignore; Marvin Init still succeeds | | Pool state never reaches `Maintenance` | KVM agent not responding | Check `cloudstack-agent` on KVM host; verify host is connected in CloudStack UI | -| iSCSI `test_10` error 530 | KVM guest does not ACK SCSI hot-unplug | Known environment limitation — see TEST_CASES.md Suite 10 note | +| iSCSI `test_09` error 530 | KVM guest does not ACK SCSI hot-unplug | Known environment limitation — see TEST_CASES.md Suite 10 note | | ONTAP REST `401 Unauthorized` | Wrong credentials in `ontap.cfg` | Verify `username`/`password` under `ontap` section | | `ISCSI protocol is not enabled on SVM` / many pool tests EXCEPTION then FAIL | SVM iSCSI service off or no `data_iscsi` LIF | Enable iSCSI on the SVM and assign `default-data-iscsi` (or `default-data-blocks`) to at least one data LIF; `run_tests.sh` now fails this check before Marvin starts | | `No ready KVM user template available` | Template still downloading | Re-run `setup_zone` (step 12 waits for template readiness); or wait in CloudStack UI | diff --git a/test/integration/plugins/ontap/TEST_CASES.md b/test/integration/plugins/ontap/TEST_CASES.md index 3e7e77e74602..2af842b2385a 100644 --- a/test/integration/plugins/ontap/TEST_CASES.md +++ b/test/integration/plugins/ontap/TEST_CASES.md @@ -19,7 +19,7 @@ # ONTAP Integration Test Cases -Complete reference for all 82 test cases across 12 test suites. +Complete reference for all 80 test cases across 12 test suites. Each suite is sequential — tests must run in numbered order; each step builds on state created by the previous step. --- @@ -114,7 +114,7 @@ Each suite is sequential — tests must run in numbered order; each step builds **File:** `nfs3/instance/test_vm_volume_attach.py` **Class:** `TestOntapVMVolumeAttach` **Tag:** `vm_volume_workflow` -**Total:** 14 tests | **Scope:** end-to-end — NFS3 pool, data volume resize, running VM (ROOT on the ONTAP pool via a tagged compute offering), primary template cache seed / reuse / survive VM delete, attach/detach lifecycle +**Total:** 13 tests | **Scope:** end-to-end — NFS3 pool, data volume resize, running VM (ROOT on the ONTAP pool via a tagged compute offering), primary template cache seed / reuse / survive VM delete, attach/detach lifecycle | # | Test method | Goal | Depends on | CloudStack success criteria | ONTAP success criteria | Type | |---|-------------|------|------------|-----------------------------|------------------------|------| @@ -124,14 +124,13 @@ Each suite is sequential — tests must run in numbered order; each step builds | 03a | `test_03a_deploy_second_vm_reuses_template_cache` | Deploy VM-2 — reuses cache | test_03 | VM-2 Running; ROOT on pool; still exactly one `template_spool_ref` | Same cache file (no second cache) | positive | | 03b | `test_03b_expunge_second_vm_template_cache_survives` | Expunge VM-2 — cache must remain (lazy GC) | test_03a | spool_ref still Ready | Cache file still present | positive | | 04 | `test_04_attach_volume_to_vm` | Attach the ONTAP data volume to the running VM (hot-plug) | test_03b (`vm`, `volume`) | `volume.virtualmachineid == vm.id`; `attachVolume` job succeeds | FlexVol `online`; after attach, a data file matching volume UUID present in FlexVol (`list_files_in_volume`) | positive | -| 05 | `test_05_resize_running_vm_rejected` | Attempt to grow an attached managed volume while its VM is running | test_04 | `resizeVolume` is rejected; volume size remains unchanged | ONTAP backing file still present; qcow2 virtual size remains unchanged | negative | -| 06 | `test_06_stop_vm_export_retained` | Stop the running VM with volume attached | test_05 | `vm.state == "Stopped"` | FlexVol still `online`; NFS export policy still present | positive | -| 07 | `test_07_grow_stopped_vm_volume` | Grow the attached volume while the VM is stopped | test_06 | Volume is `Ready` at the requested byte size | `qemu-img info` reports the requested virtual size; ONTAP file remains present | positive | -| 08 | `test_08_shrink_rejected` | Attempt to shrink the grown qcow2 volume with `shrinkok=true` | test_07 | Resize is rejected; grown size remains unchanged | qcow2 virtual size remains unchanged | negative | -| 09 | `test_09_start_vm_volume_accessible` | Start the stopped VM | test_08 | `vm.state == "Running"`; KVM data-disk capacity equals target | FlexVol still `online` | positive | -| 10 | `test_10_detach_volume_from_vm` | Hot-detach the ONTAP volume from the running VM (TDS Detach NFS3) | test_09 (`vm`, `volume`) | `volume.virtualmachineid` cleared; `volume.state == "Ready"` | FlexVol still `online`; data file **still present** (NFS3: file persists until `deleteVolume`, not on detach) | positive | -| 11 | `test_11_grow_detached_volume` | Grow the detached, materialized data volume | test_10 | API and DB size equal target | qcow2 virtual size equals target | positive | -| 12 | `test_12_destroy_vm_and_cleanup` | Destroy VM (expunge), delete volume, enter maintenance, force-delete pool | test_11 | VM no longer listed; spool_ref still Ready after VM expunge; volume no longer listed; pool no longer listed | Cache file present after VM expunge; FlexVol deleted; export policy deleted | cleanup | +| 05 | `test_05_stop_vm_export_retained` | Stop the running VM with volume attached | test_04 | `vm.state == "Stopped"` | FlexVol still `online`; NFS export policy still present | positive | +| 06 | `test_06_grow_stopped_vm_volume` | Grow the attached volume while the VM is stopped | test_05 | Volume is `Ready` at the requested byte size | `qemu-img info` reports the requested virtual size; ONTAP file remains present | positive | +| 07 | `test_07_shrink_rejected` | Attempt to shrink the grown qcow2 volume with `shrinkok=true` | test_06 | Resize is rejected; grown size remains unchanged | qcow2 virtual size remains unchanged | negative | +| 08 | `test_08_start_vm_volume_accessible` | Start the stopped VM | test_07 | `vm.state == "Running"`; KVM data-disk capacity equals target | FlexVol still `online` | positive | +| 09 | `test_09_detach_volume_from_vm` | Hot-detach the ONTAP volume from the running VM (TDS Detach NFS3) | test_08 (`vm`, `volume`) | `volume.virtualmachineid` cleared; `volume.state == "Ready"` | FlexVol still `online`; data file **still present** (NFS3: file persists until `deleteVolume`, not on detach) | positive | +| 10 | `test_10_grow_detached_volume` | Grow the detached, materialized data volume | test_09 | API and DB size equal target | qcow2 virtual size equals target | positive | +| 11 | `test_11_destroy_vm_and_cleanup` | Destroy VM (expunge), delete volume, enter maintenance, force-delete pool | test_10 | VM no longer listed; spool_ref still Ready after VM expunge; volume no longer listed; pool no longer listed | Cache file present after VM expunge; FlexVol deleted; export policy deleted | cleanup | --- @@ -212,7 +211,7 @@ Each suite is sequential — tests must run in numbered order; each step builds **File:** `iscsi/instance/test_vm_volume_attach.py` **Class:** `TestOntapVMVolumeAttachISCSI` **Tag:** `iscsi_vm_workflow` -**Total:** 14 tests | **Scope:** end-to-end — iSCSI pool, data volume resize (LUN), running VM (ROOT on the ONTAP pool via a tagged compute offering), `cs_tmpl_` LUN cache seed / reuse / survive VM delete, attach/stop/start/detach lifecycle +**Total:** 13 tests | **Scope:** end-to-end — iSCSI pool, data volume resize (LUN), running VM (ROOT on the ONTAP pool via a tagged compute offering), `cs_tmpl_` LUN cache seed / reuse / survive VM delete, attach/stop/start/detach lifecycle | # | Test method | Goal | Depends on | CloudStack success criteria | ONTAP success criteria | Type | |---|-------------|------|------------|-----------------------------|------------------------|------| @@ -222,16 +221,15 @@ Each suite is sequential — tests must run in numbered order; each step builds | 03a | `test_03a_deploy_second_vm_reuses_template_cache` | Deploy VM-2 — reuse cache | test_03 | VM-2 Running; ROOT on pool; still one spool_ref | Still one `cs_tmpl_*`; non-cache LUN count +1 | positive | | 03b | `test_03b_expunge_second_vm_template_cache_survives` | Expunge VM-2 — cache LUN remains | test_03a | spool_ref still Ready | VM-2 ROOT LUN gone (non-cache count back to baseline); `cs_tmpl_*` still present | positive | | 04 | `test_04_attach_volume_to_vm` | Hot-attach the ONTAP iSCSI volume to the running VM — a LUN-map is created (TDS SN 27) | test_03b (`vm`, `volume`) | `volume.virtualmachineid == vm.id` | ≥1 LUN-map linking the LUN to the host's igroup | positive | -| 05 | `test_05_resize_running_vm_rejected` | Attempt to grow the attached managed LUN while its VM is running | test_04 | `resizeVolume` is rejected; volume size remains unchanged | LUN `space.size` remains unchanged | negative | -| 06 | `test_06_stop_vm_lun_unmapped` | Stop VM — LUN-maps must be removed (TDS VM Stop iSCSI) | test_05 | `vm.state == "Stopped"` | 0 LUN-maps; LUN itself **still present** in FlexVol | positive | -| 07 | `test_07_grow_stopped_vm_volume` | Grow the attached volume while the VM is stopped | test_06 | Volume is `Ready` at the requested byte size | LUN `space.size` equals the requested size; LUN remains unmapped | positive | -| 08 | `test_08_shrink_rejected` | Attempt to shrink the grown LUN with `shrinkok=true` | test_07 | Resize is rejected; grown size remains unchanged | LUN `space.size` remains unchanged | negative | -| 09 | `test_09_start_vm_lun_remapped` | Start VM — LUN-maps must be re-created (TDS VM Start iSCSI) | test_08 | `vm.state == "Running"`; KVM data-disk capacity equals target | ≥1 LUN-map re-created | positive | -| 10 | `test_10_detach_volume_from_vm` | Hot-detach the iSCSI volume from the running VM (TDS Detach iSCSI) | test_09 (`vm`, `volume`) | `volume.virtualmachineid` cleared | 0 LUN-maps; LUN still in FlexVol | positive ⚠️ | -| 11 | `test_11_grow_detached_volume` | Grow the detached, materialized data volume | test_10 | API and DB size equal target | LUN `space.size` equals target | positive | -| 12 | `test_12_destroy_vm_and_cleanup` | Destroy VM (expunge), delete volume, enter maintenance, force-delete pool | test_11 | VM gone; spool_ref still Ready after VM expunge; volume gone; pool gone | `cs_tmpl_*` present after VM expunge; FlexVol deleted; all LUNs and igroups deleted | cleanup | +| 05 | `test_05_stop_vm_lun_unmapped` | Stop VM — LUN-maps must be removed (TDS VM Stop iSCSI) | test_04 | `vm.state == "Stopped"` | 0 LUN-maps; LUN itself **still present** in FlexVol | positive | +| 06 | `test_06_grow_stopped_vm_volume` | Grow the attached volume while the VM is stopped | test_05 | Volume is `Ready` at the requested byte size | LUN `space.size` equals the requested size; LUN remains unmapped | positive | +| 07 | `test_07_shrink_rejected` | Attempt to shrink the grown LUN with `shrinkok=true` | test_06 | Resize is rejected; grown size remains unchanged | LUN `space.size` remains unchanged | negative | +| 08 | `test_08_start_vm_lun_remapped` | Start VM — LUN-maps must be re-created (TDS VM Start iSCSI) | test_07 | `vm.state == "Running"`; KVM data-disk capacity equals target | ≥1 LUN-map re-created | positive | +| 09 | `test_09_detach_volume_from_vm` | Hot-detach the iSCSI volume from the running VM (TDS Detach iSCSI) | test_08 (`vm`, `volume`) | `volume.virtualmachineid` cleared | 0 LUN-maps; LUN still in FlexVol | positive ⚠️ | +| 10 | `test_10_grow_detached_volume` | Grow the detached, materialized data volume | test_09 | API and DB size equal target | LUN `space.size` equals target | positive | +| 11 | `test_11_destroy_vm_and_cleanup` | Destroy VM (expunge), delete volume, enter maintenance, force-delete pool | test_10 | VM gone; spool_ref still Ready after VM expunge; volume gone; pool gone | `cs_tmpl_*` present after VM expunge; FlexVol deleted; all LUNs and igroups deleted | cleanup | -> ⚠️ **test_10 known status:** iSCSI hot-detach from a running VM relies on the KVM guest acknowledging the SCSI device removal. On this environment the guest does not acknowledge in time, causing CloudStack error 530. This is a KVM-host-level or guest-template limitation, not a test code defect. +> ⚠️ **test_09 known status:** iSCSI hot-detach from a running VM relies on the KVM guest acknowledging the SCSI device removal. On this environment the guest does not acknowledge in time, causing CloudStack error 530. This is a KVM-host-level or guest-template limitation, not a test code defect. --- @@ -273,12 +271,12 @@ Each suite is sequential — tests must run in numbered order; each step builds | NFS3 Pool with Volumes | NFS3 | Cluster | 7 | ✅ | | NFS3 Zone-Scoped Pool | NFS3 | Zone | 4 | ✅ | | NFS3 Volume Lifecycle | NFS3 | Cluster | 5 | ✅ | -| NFS3 VM + Volume Attach | NFS3 | Cluster | 14 | ⏳ resize cases pending lab validation | +| NFS3 VM + Volume Attach | NFS3 | Cluster | 13 | ⏳ resize cases pending lab validation | | NFS3 Template Cache Negative | NFS3 | Cluster | 3 | 🆕 | | iSCSI Pool Lifecycle | iSCSI | Cluster | 8 | ✅ | | iSCSI Pool with Volumes | iSCSI | Cluster | 7 | ✅ | | iSCSI Zone-Scoped Pool | iSCSI | Zone | 4 | ✅ | | iSCSI Volume Lifecycle | iSCSI | Cluster | 5 | ✅ | -| iSCSI VM + Volume Attach | iSCSI | Cluster | 14 | ⏳ resize cases pending lab validation; detach limitation | +| iSCSI VM + Volume Attach | iSCSI | Cluster | 13 | ⏳ resize cases pending lab validation; detach limitation | | iSCSI Template Cache Negative | iSCSI | Cluster | 3 | 🆕 | -| **Total** | | | **82** | **New resize cases pending lab validation** | +| **Total** | | | **80** | **New resize cases pending lab validation** | diff --git a/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py b/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py index 5fd2ae60e6a6..49dde8d1828e 100644 --- a/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py +++ b/test/integration/plugins/ontap/iscsi/instance/test_vm_volume_attach.py @@ -34,7 +34,7 @@ - startVirtualMachine → LUN-map is re-created - detachVolume → LUN-map is removed -Tests are numbered test_01 ... test_12, plus test_03a and test_03b, and must +Tests are numbered test_01 ... test_11, plus test_03a and test_03b, and must run in that order. Each step builds on the shared state established by the previous step. @@ -52,14 +52,13 @@ 03a Deploy a second VM — reuses the cache LUN (one more ROOT clone LUN only) 03b Expunge the second VM — its ROOT LUN is removed; cache LUN survives 04 Attach iSCSI data volume to running VM (LUN-map created) - 05 Verify resize is rejected while the attached VM is running - 06 Stop VM — LUN-map for attached volume is removed from ONTAP - 07 Grow the attached volume and ONTAP LUN while the VM is stopped - 08 Verify shrinking the ONTAP LUN is rejected without changing its size - 09 Start VM — LUN-map is re-created on ONTAP - 10 Detach data volume from running VM — LUN-map removed - 11 Grow the detached data volume - 12 Destroy VM (cache LUN still survives), delete data volume, delete pool + 05 Stop VM — LUN-map for attached volume is removed from ONTAP + 06 Grow the attached volume and ONTAP LUN while the VM is stopped + 07 Verify shrinking the ONTAP LUN is rejected without changing its size + 08 Start VM — LUN-map is re-created on ONTAP + 09 Detach data volume from running VM — LUN-map removed + 10 Grow the detached data volume + 11 Destroy VM (cache LUN still survives), delete data volume, delete pool Prerequisites: - CloudStack management server with the NetApp ONTAP plugin deployed @@ -326,7 +325,7 @@ def setUpClass(cls): def tearDownClass(cls): """ Safety-net cleanup: destroy VMs if still alive, delete the guest - network created for Advanced zones (if not already deleted by test_12), + network created for Advanced zones (if not already deleted by test_11), then delegate pool/volume/account cleanup to the base class. """ for vm in (cls.vm2, cls.vm): @@ -349,7 +348,7 @@ def tearDownClass(cls): cls.vm = None # Delete the guest network created for this account in Advanced zones. - # test_12 deletes it on the happy path; this is the fallback for + # test_11 deletes it on the happy path; this is the fallback for # mid-suite failures. if cls._created_network_id is not None: try: @@ -727,51 +726,11 @@ def test_04_attach_volume_to_vm(self): "Initial ONTAP LUN size should match the CloudStack volume size") # ------------------------------------------------------------------ - # Step 05 — Resize attached volume while VM is running must fail + # Step 05 — Stop VM — LUN-maps should be removed # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_05_resize_running_vm_rejected(self): - """ - Attempt to grow the attached iSCSI volume while its managed KVM VM is - running. - Verifies: - - CloudStack rejects the resize - - CloudStack volume and ONTAP LUN sizes remain unchanged - """ - if self.__class__.vm is None: - self.skipTest("VM not deployed — test_03 was skipped") - self.assertIsNotNone(self.__class__.volume, - "Volume absent — test_02 must pass first") - - target_gib = self.__class__.target_volume_size // (1024 ** 3) - with self.assertRaises(Exception) as context: - self._resize_volume(self.__class__.volume.id, target_gib) - self.assertIn( - "not in the Stopped state", str(context.exception), - "Resize failed for an unexpected reason") - - volume = self._poll_volume_size( - self.__class__.volume.id, self.__class__.initial_volume_size) - self.assertIsNotNone( - volume, "CloudStack volume size changed after rejected resize") - - # DB: size must remain unchanged after a rejected resize - db_size = self._get_db_volume_size(self.__class__.volume.id) - self.assertEqual( - db_size, self.__class__.initial_volume_size, - "cloud DB volumes.size changed after rejected running-VM resize") - - self.assertEqual( - self._data_lun_size(), self.__class__.initial_volume_size, - "ONTAP LUN size changed after rejected running-VM resize") - - # ------------------------------------------------------------------ - # Step 06 — Stop VM — LUN-maps should be removed - # ------------------------------------------------------------------ - - @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_06_stop_vm_lun_unmapped(self): + def test_05_stop_vm_lun_unmapped(self): """ Stop the running VM while the iSCSI data volume is still attached. Covers TDS VM Stop (iSCSI): 'Luns for the volumes under this VM @@ -810,11 +769,11 @@ def test_06_stop_vm_lun_unmapped(self): ) # ------------------------------------------------------------------ - # Step 07 — Grow attached volume while VM is stopped + # Step 06 — Grow attached volume while VM is stopped # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_07_grow_stopped_vm_volume(self): + def test_06_grow_stopped_vm_volume(self): """ Grow the attached iSCSI volume while its managed KVM VM is stopped. Verifies: @@ -852,11 +811,11 @@ def test_07_grow_stopped_vm_volume(self): "The data LUN should remain unmapped while the VM is stopped") # ------------------------------------------------------------------ - # Step 08 — Shrink must fail + # Step 07 — Shrink must fail # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_08_shrink_rejected(self): + def test_07_shrink_rejected(self): """ Attempt to shrink the grown iSCSI volume back to its initial size. Verifies: @@ -870,7 +829,7 @@ def test_08_shrink_rejected(self): self.assertEqual( self._get_db_volume_size(self.__class__.volume.id), self.__class__.target_volume_size, - "Volume was not grown to the target size — test_07 must pass " + "Volume was not grown to the target size — test_06 must pass " "before the shrink check") initial_gib = self.__class__.initial_volume_size // (1024 ** 3) @@ -898,11 +857,11 @@ def test_08_shrink_rejected(self): "ONTAP LUN size changed after rejected shrink") # ------------------------------------------------------------------ - # Step 09 — Start VM — LUN-maps should be re-created + # Step 08 — Start VM — LUN-maps should be re-created # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_09_start_vm_lun_remapped(self): + def test_08_start_vm_lun_remapped(self): """ Start the stopped VM. Covers TDS VM Start (iSCSI): 'luns should be re-mapped again to @@ -933,11 +892,11 @@ def test_09_start_vm_lun_remapped(self): ) # ------------------------------------------------------------------ - # Step 10 — Detach volume from running VM + # Step 09 — Detach volume from running VM # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_10_detach_volume_from_vm(self): + def test_09_detach_volume_from_vm(self): """ Detach the iSCSI data volume from the running VM. Verifies: @@ -996,11 +955,11 @@ def test_10_detach_volume_from_vm(self): ) # ------------------------------------------------------------------ - # Step 11 — Grow detached data volume + # Step 10 — Grow detached data volume # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_11_grow_detached_volume(self): + def test_10_grow_detached_volume(self): """Grow the materialized LUN after it has been detached.""" self.assertIsNotNone(self.__class__.volume, "Volume absent — test_02 must pass first") @@ -1019,11 +978,11 @@ def test_11_grow_detached_volume(self): self.__class__.target_volume_size = detached_target # ------------------------------------------------------------------ - # Step 12 — Destroy VM and clean up pool + # Step 11 — Destroy VM and clean up pool # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_12_destroy_vm_and_cleanup(self): + def test_11_destroy_vm_and_cleanup(self): """ Destroy the VM (with expunge), delete the data volume, force-delete the ONTAP pool, and delete the guest network created for this suite. diff --git a/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py b/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py index a8d1d6ae7415..af63b4cd9d3f 100644 --- a/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py +++ b/test/integration/plugins/ontap/nfs3/instance/test_vm_volume_attach.py @@ -19,7 +19,7 @@ Sequential workflow integration tests for NetApp ONTAP data volume lifecycle with a running virtual machine. -Tests are numbered test_01 ... test_12, plus test_03a and test_03b, and must +Tests are numbered test_01 ... test_11, plus test_03a and test_03b, and must run in that order. Each step builds on the shared state established by the previous step. @@ -36,14 +36,13 @@ 03a Deploy a second VM — reuses the cache (still one template_spool_ref) 03b Expunge the second VM — cache and template_spool_ref survive 04 Attach the ONTAP data volume to the running VM - 05 Verify resize is rejected while the attached VM is running - 06 Stop the VM — export policy stays; volume remains attached in CS - 07 Grow the attached volume and its qcow2 file while the VM is stopped - 08 Verify shrinking the volume is rejected without changing its size - 09 Start the VM — VM Running; volume still attached; FlexVol online - 10 Detach the ONTAP data volume from the VM - 11 Grow the detached data volume - 12 Destroy VM (cache still survives); delete ONTAP volume; enter + 05 Stop the VM — export policy stays; volume remains attached in CS + 06 Grow the attached volume and its qcow2 file while the VM is stopped + 07 Verify shrinking the volume is rejected without changing its size + 08 Start the VM — VM Running; volume still attached; FlexVol online + 09 Detach the ONTAP data volume from the VM + 10 Grow the detached data volume + 11 Destroy VM (cache still survives); delete ONTAP volume; enter maintenance; force-delete pool Prerequisites: @@ -652,60 +651,11 @@ def test_04_attach_volume_to_vm(self): ) # ------------------------------------------------------------------ - # Step 05 - Resize attached volume while VM is running must fail + # Step 05 - Stop VM — export policy must be retained # ------------------------------------------------------------------ @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_05_resize_running_vm_rejected(self): - """ - Attempt to grow the attached volume while its managed KVM VM is - running. - Verifies: - - CloudStack rejects the resize while the VM is running - - CloudStack volume size is unchanged - - The ONTAP backing file is still present in the FlexVol - - qcow2 virtual size is unchanged - """ - if self.__class__.vm is None: - self.skipTest("VM not deployed — test_03 was skipped") - self.assertIsNotNone(self.__class__.volume, - "Volume absent — test_02 must pass first") - - target_gib = self.__class__.target_volume_size // (1024 ** 3) - with self.assertRaises(Exception) as context: - self._resize_volume(self.__class__.volume.id, target_gib) - self.assertIn( - "not in the Stopped state", str(context.exception), - "Resize failed for an unexpected reason") - - volume = self._poll_volume_size( - self.__class__.volume.id, self.__class__.initial_volume_size) - self.assertIsNotNone( - volume, "CloudStack volume size changed after rejected resize") - - # DB: size must remain unchanged after a rejected resize - db_size = self._get_db_volume_size(self.__class__.volume.id) - self.assertEqual( - db_size, self.__class__.initial_volume_size, - "cloud DB volumes.size changed after rejected running-VM resize") - - files = self.ontap.list_files_in_volume(self.__class__.pool.name) - self.assertIn( - self.__class__.volume_path, files, - "ONTAP backing file disappeared after the rejected running-VM resize") - - backend_size = self._get_qcow2_virtual_size( - self.__class__.pool.id, self.__class__.volume_path) - self.assertEqual( - backend_size, self.__class__.initial_volume_size, - "qcow2 virtual size changed after rejected running-VM resize") - - # ------------------------------------------------------------------ - # Step 06 - Stop VM — export policy must be retained - # ------------------------------------------------------------------ - - @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_06_stop_vm_export_retained(self): + def test_05_stop_vm_export_retained(self): """ Stop the running VM while the NFS3 data volume is still attached. Unlike iSCSI (where LUN-maps are removed on VM stop), NFS3 export @@ -759,11 +709,11 @@ def test_06_stop_vm_export_retained(self): ) # ------------------------------------------------------------------ - # Step 07 - Grow attached volume while VM is stopped + # Step 06 - Grow attached volume while VM is stopped # ------------------------------------------------------------------ @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_07_grow_stopped_vm_volume(self): + def test_06_grow_stopped_vm_volume(self): """ Grow the attached NFS3 volume while its managed KVM VM is stopped. Verifies: @@ -809,11 +759,11 @@ def test_07_grow_stopped_vm_volume(self): "NFS3 backing file disappeared from ONTAP after resize") # ------------------------------------------------------------------ - # Step 08 - Shrink must fail + # Step 07 - Shrink must fail # ------------------------------------------------------------------ @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_08_shrink_rejected(self): + def test_07_shrink_rejected(self): """ Attempt to shrink the grown NFS3 volume back to its initial size. Verifies: @@ -827,7 +777,7 @@ def test_08_shrink_rejected(self): self.assertEqual( self._get_db_volume_size(self.__class__.volume.id), self.__class__.target_volume_size, - "Volume was not grown to the target size — test_07 must pass " + "Volume was not grown to the target size — test_06 must pass " "before the shrink check") initial_gib = self.__class__.initial_volume_size // (1024 ** 3) @@ -857,11 +807,11 @@ def test_08_shrink_rejected(self): "qcow2 virtual size changed after rejected shrink") # ------------------------------------------------------------------ - # Step 09 - Start VM — volume accessible; FlexVol online + # Step 08 - Start VM — volume accessible; FlexVol online # ------------------------------------------------------------------ @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_09_start_vm_volume_accessible(self): + def test_08_start_vm_volume_accessible(self): """ Start the stopped VM. Verifies: @@ -921,11 +871,11 @@ def test_09_start_vm_volume_accessible(self): "KVM data-disk capacity does not reflect the NFS3 resize") # ------------------------------------------------------------------ - # Step 10 - Detach ONTAP data volume from the VM + # Step 09 - Detach ONTAP data volume from the VM # ------------------------------------------------------------------ @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_10_detach_volume_from_vm(self): + def test_09_detach_volume_from_vm(self): """ Detach the ONTAP data volume from the running VM. Verifies: @@ -1008,11 +958,11 @@ def test_10_detach_volume_from_vm(self): ) # ------------------------------------------------------------------ - # Step 11 - Grow detached data volume + # Step 10 - Grow detached data volume # ------------------------------------------------------------------ @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_11_grow_detached_volume(self): + def test_10_grow_detached_volume(self): """Grow the materialized qcow2 after it has been detached.""" self.assertIsNotNone(self.__class__.volume, "Volume absent — test_02 must pass first") @@ -1033,11 +983,11 @@ def test_11_grow_detached_volume(self): self.__class__.target_volume_size = detached_target # ------------------------------------------------------------------ - # Step 12 - Destroy VM, delete volume, delete pool + # Step 11 - Destroy VM, delete volume, delete pool # ------------------------------------------------------------------ @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_12_destroy_vm_and_cleanup(self): + def test_11_destroy_vm_and_cleanup(self): """ Destroy the VM, delete the ONTAP data volume, enter maintenance, then force-delete the pool.