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)); + } }