diff --git a/test/integration/plugins/ontap/README.md b/test/integration/plugins/ontap/README.md index 2adbf449af28..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 (72 tests) +├── TEST_CASES.md # Full test case reference table (80 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` | 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` | 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` | 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_07` 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 d3f2b1be58d1..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 72 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:** 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:** 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 | |---|-------------|------|------------|-----------------------------|------------------------|------| @@ -123,11 +123,14 @@ 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 | +| 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_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 | +| 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 | --- @@ -208,22 +211,25 @@ 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:** 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 | |---|-------------|------|------------|-----------------------------|------------------------|------| | 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 | +| 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_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 | +| 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_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. +> ⚠️ **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. --- @@ -265,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 | 10 | 🆕 +2 template cache | +| 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 | 10 | ⚠️ 7/8 + 🆕 2 template cache | +| iSCSI VM + Volume Attach | iSCSI | Cluster | 13 | ⏳ resize cases pending lab validation; detach limitation | | iSCSI Template Cache Negative | iSCSI | Cluster | 3 | 🆕 | -| **Total** | | | **72** | | +| **Total** | | | **80** | **New resize cases pending lab validation** | 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 2e247c5e2f87..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,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_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. 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: @@ -53,9 +53,12 @@ 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 + 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 @@ -215,6 +218,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 +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_08), + 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): @@ -343,7 +348,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_11 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 +461,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 +514,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 +536,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,6 +716,15 @@ 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 — Stop VM — LUN-maps should be removed # ------------------------------------------------------------------ @@ -729,11 +769,99 @@ def test_05_stop_vm_lun_unmapped(self): ) # ------------------------------------------------------------------ - # Step 06 — Start VM — LUN-maps should be re-created + # Step 06 — Grow attached volume while VM is stopped + # ------------------------------------------------------------------ + + @attr(tags=["iscsi_vm_workflow"], required_hardware=True) + def test_06_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 07 — Shrink must fail # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_06_start_vm_lun_remapped(self): + def test_07_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_06 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 volume", 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 08 — Start VM — LUN-maps should be re-created + # ------------------------------------------------------------------ + + @attr(tags=["iscsi_vm_workflow"], required_hardware=True) + def test_08_start_vm_lun_remapped(self): """ Start the stopped VM. Covers TDS VM Start (iSCSI): 'luns should be re-mapped again to @@ -764,11 +892,11 @@ def test_06_start_vm_lun_remapped(self): ) # ------------------------------------------------------------------ - # Step 07 — Detach volume from running VM + # Step 09 — Detach volume from running VM # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_07_detach_volume_from_vm(self): + def test_09_detach_volume_from_vm(self): """ Detach the iSCSI data volume from the running VM. Verifies: @@ -827,11 +955,34 @@ def test_07_detach_volume_from_vm(self): ) # ------------------------------------------------------------------ - # Step 08 — Destroy VM and clean up pool + # Step 10 — Grow detached data volume + # ------------------------------------------------------------------ + + @attr(tags=["iscsi_vm_workflow"], required_hardware=True) + 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") + 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 11 — Destroy VM and clean up pool # ------------------------------------------------------------------ @attr(tags=["iscsi_vm_workflow"], required_hardware=True) - def test_08_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 dbd6b210ce85..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,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_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. 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) @@ -37,9 +37,12 @@ 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 + 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: @@ -178,6 +181,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,6 +635,21 @@ 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 - Stop VM — export policy must be retained # ------------------------------------------------------------------ @@ -688,11 +709,109 @@ def test_05_stop_vm_export_retained(self): ) # ------------------------------------------------------------------ - # Step 06 - Start VM — volume accessible; FlexVol online + # Step 06 - Grow attached volume while VM is stopped + # ------------------------------------------------------------------ + + @attr(tags=["vm_volume_workflow"], required_hardware=True) + def test_06_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 07 - Shrink must fail # ------------------------------------------------------------------ @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_06_start_vm_volume_accessible(self): + def test_07_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_06 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 08 - Start VM — volume accessible; FlexVol online + # ------------------------------------------------------------------ + + @attr(tags=["vm_volume_workflow"], required_hardware=True) + def test_08_start_vm_volume_accessible(self): """ Start the stopped VM. Verifies: @@ -743,12 +862,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 09 - 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_09_detach_volume_from_vm(self): """ Detach the ONTAP data volume from the running VM. Verifies: @@ -831,11 +958,36 @@ def test_07_detach_volume_from_vm(self): ) # ------------------------------------------------------------------ - # Step 08 - Destroy VM, delete volume, delete pool + # Step 10 - Grow detached data volume + # ------------------------------------------------------------------ + + @attr(tags=["vm_volume_workflow"], required_hardware=True) + 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") + 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 11 - Destroy VM, delete volume, delete pool # ------------------------------------------------------------------ @attr(tags=["vm_volume_workflow"], required_hardware=True) - def test_08_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. diff --git a/test/integration/plugins/ontap/ontap_test_base.py b/test/integration/plugins/ontap/ontap_test_base.py index 17bec2c1251c..2858bd7e25aa 100644 --- a/test/integration/plugins/ontap/ontap_test_base.py +++ b/test/integration/plugins/ontap/ontap_test_base.py @@ -45,16 +45,17 @@ listDiskOfferings as listDiskOfferingsAPI, listVirtualMachines as listVirtualMachinesAPI, listVolumes as listVolumesAPI, + resizeVolume as resizeVolumeAPI, updateStoragePool as updateStoragePoolAPI, ) from marvin.cloudstackAPI import listHosts as listHostsAPI 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) @@ -258,7 +259,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 +268,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 +527,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. @@ -567,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 ----------------------------------------------- @@ -894,6 +862,71 @@ 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.""" + 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.""" + 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 + ``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()