From c2202defbd4752a1b036679378eb4099e0fdf0dd Mon Sep 17 00:00:00 2001 From: Brad House Date: Fri, 6 Nov 2026 21:21:13 +0000 Subject: [PATCH] Allow growing a running KVM VM's ROOT volume through a new disk offering A size-only resizeVolume of a ROOT volume already works on a running KVM VM: orchestrateResizeVolume checks that the host is Up and Enabled, and the agent grows the disk in place with virsh blockresize. Growing the same volume by giving it a larger disk offering is refused, through resizeVolume with diskofferingid, through changeOfferingForVolume, and so through scaleVirtualMachine when allow.diskoffering.change.during.scale.vm is on: Cannot resize ROOT volume [...] when VM is not on Stopped State checkIfVolumeIsRootAndVmIsRunning, which the size-only path does not call, refuses any size change of a ROOT volume unless the VM is Stopped, on every hypervisor. Where the ROOT volume's offering fixes its size, the size-only path is refused too, so stopping the VM was the only way to grow its root disk. Let the check through when the volume belongs to a Running KVM user VM and the new size is larger than the current one. Shrinking, other hypervisors, system VMs and other VM states are refused as before. The resize then follows the same path as a size-only resize of a running VM. Signed-off-by: Brad House --- .../cloud/storage/VolumeApiServiceImpl.java | 19 ++++++- .../storage/VolumeApiServiceImplTest.java | 56 +++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 9b2f4f831d3e..d5443662e23d 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -1524,13 +1524,26 @@ protected boolean isNotPossibleToResize(VolumeVO volume, DiskOfferingVO diskOffe return isOfferingEnforcingRootDiskSize && isRoot && isNotIso; } - private void checkIfVolumeIsRootAndVmIsRunning(Long newSize, VolumeVO volume, VMInstanceVO vmInstanceVO) { - if (!volume.getSize().equals(newSize) && volume.getVolumeType().equals(Volume.Type.ROOT) && !State.Stopped.equals(vmInstanceVO.getState())) { - throw new InvalidParameterValueException(String.format("Cannot resize ROOT volume [%s] when VM is not on Stopped State. VM %s is in state %s", volume.getName(), vmInstanceVO + protected void checkIfVolumeIsRootAndVmIsRunning(Long newSize, VolumeVO volume, VMInstanceVO vmInstanceVO) { + if (!volume.getSize().equals(newSize) && volume.getVolumeType().equals(Volume.Type.ROOT) && !State.Stopped.equals(vmInstanceVO.getState()) + && !isGrowOfVolumeOnRunningKvmVm(newSize, volume, vmInstanceVO)) { + throw new InvalidParameterValueException(String.format("Cannot resize ROOT volume [%s] when VM is not on Stopped State. VM %s is in state %s. " + + "Only growing the ROOT volume of a Running KVM user VM is allowed without stopping it.", volume.getName(), vmInstanceVO .getInstanceName(), vmInstanceVO.getState())); } } + /** + * KVM grows the disk of a running user VM in place (virsh blockresize), and a size-only resizeVolume of a + * ROOT volume already does so. Growing it through a new disk offering is the same operation. + * Shrinking still requires the VM to be stopped. Only user VMs are let through, since the resize orchestration + * (orchestrateResizeVolume) only looks up the running VM and its host for user VMs. + */ + private boolean isGrowOfVolumeOnRunningKvmVm(Long newSize, VolumeVO volume, VMInstanceVO vmInstanceVO) { + return HypervisorType.KVM.equals(vmInstanceVO.getHypervisorType()) && VirtualMachine.Type.User.equals(vmInstanceVO.getType()) + && State.Running.equals(vmInstanceVO.getState()) && newSize != null && newSize > volume.getSize(); + } + private void validateIops(Long minIops, Long maxIops, Storage.StoragePoolType poolType) { if (poolType == Storage.StoragePoolType.PowerFlex) { // PowerFlex takes iopsLimit as input, skip minIops validation diff --git a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java index b806eb4afe5c..4e60aeec2f80 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -139,6 +139,7 @@ import com.cloud.vm.DiskProfile; import com.cloud.vm.UserVmManager; import com.cloud.vm.UserVmVO; +import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.VirtualMachineManager; @@ -2504,4 +2505,59 @@ public void testResizeVolumeInternal_VMware_VMRunning_ShouldThrowStateGuardError e.getMessage() != null && e.getMessage().contains("VM should be in")); } } + + private VolumeVO mockRootVolumeOfSize(long size) { + VolumeVO volume = Mockito.mock(VolumeVO.class); + Mockito.when(volume.getSize()).thenReturn(size); + Mockito.when(volume.getVolumeType()).thenReturn(Volume.Type.ROOT); + return volume; + } + + private VMInstanceVO mockVm(HypervisorType hypervisorType, State state) { + VMInstanceVO vm = Mockito.mock(VMInstanceVO.class); + Mockito.lenient().when(vm.getHypervisorType()).thenReturn(hypervisorType); + Mockito.lenient().when(vm.getType()).thenReturn(VirtualMachine.Type.User); + Mockito.when(vm.getState()).thenReturn(state); + return vm; + } + + @Test + public void checkIfVolumeIsRootAndVmIsRunningAllowsGrowOnRunningKvmVm() { + volumeApiServiceImpl.checkIfVolumeIsRootAndVmIsRunning(20L << 30, mockRootVolumeOfSize(10L << 30), mockVm(HypervisorType.KVM, State.Running)); + } + + @Test(expected = InvalidParameterValueException.class) + public void checkIfVolumeIsRootAndVmIsRunningRefusesShrinkOnRunningKvmVm() { + volumeApiServiceImpl.checkIfVolumeIsRootAndVmIsRunning(5L << 30, mockRootVolumeOfSize(10L << 30), mockVm(HypervisorType.KVM, State.Running)); + } + + @Test(expected = InvalidParameterValueException.class) + public void checkIfVolumeIsRootAndVmIsRunningRefusesGrowOnRunningNonKvmVm() { + volumeApiServiceImpl.checkIfVolumeIsRootAndVmIsRunning(20L << 30, mockRootVolumeOfSize(10L << 30), mockVm(HypervisorType.VMware, State.Running)); + } + + @Test(expected = InvalidParameterValueException.class) + public void checkIfVolumeIsRootAndVmIsRunningRefusesGrowOnRunningKvmSystemVm() { + VMInstanceVO vm = mockVm(HypervisorType.KVM, State.Running); + Mockito.when(vm.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + volumeApiServiceImpl.checkIfVolumeIsRootAndVmIsRunning(20L << 30, mockRootVolumeOfSize(10L << 30), vm); + } + + @Test(expected = InvalidParameterValueException.class) + public void checkIfVolumeIsRootAndVmIsRunningRefusesGrowOnKvmVmNotRunning() { + volumeApiServiceImpl.checkIfVolumeIsRootAndVmIsRunning(20L << 30, mockRootVolumeOfSize(10L << 30), mockVm(HypervisorType.KVM, State.Migrating)); + } + + @Test + public void checkIfVolumeIsRootAndVmIsRunningAllowsResizeOnStoppedVm() { + volumeApiServiceImpl.checkIfVolumeIsRootAndVmIsRunning(5L << 30, mockRootVolumeOfSize(10L << 30), mockVm(HypervisorType.VMware, State.Stopped)); + } + + @Test + public void checkIfVolumeIsRootAndVmIsRunningIgnoresDataDisk() { + VolumeVO volume = Mockito.mock(VolumeVO.class); + Mockito.when(volume.getSize()).thenReturn(10L << 30); + Mockito.when(volume.getVolumeType()).thenReturn(Volume.Type.DATADISK); + volumeApiServiceImpl.checkIfVolumeIsRootAndVmIsRunning(5L << 30, volume, Mockito.mock(VMInstanceVO.class)); + } }