This is an automated email from the ASF dual-hosted git repository.
harikrishna-patnala pushed a commit to branch 4.22
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.22 by this push:
new 7c2a8768e67 check state instead of power state on change offering for
ROOT volume (#13271)
7c2a8768e67 is described below
commit 7c2a8768e67409247cc592d043044d165d5dd506
Author: dahn <[email protected]>
AuthorDate: Mon Jul 6 11:11:25 2026 +0200
check state instead of power state on change offering for ROOT volume
(#13271)
* check state instead of power state on change offering for ROOT volume
* tests
* Apply suggestions from code review
Co-authored-by: Fabricio Duarte <[email protected]>
Co-authored-by: dahn <[email protected]>
* Apply test by package private
* fixing tests
---------
Co-authored-by: Daan Hoogland <[email protected]>
Co-authored-by: Fabricio Duarte <[email protected]>
---
.../com/cloud/storage/VolumeApiServiceImpl.java | 22 ++-
.../cloud/storage/VolumeApiServiceImplTest.java | 202 ++++++++++++++++++++-
2 files changed, 206 insertions(+), 18 deletions(-)
diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
index 687e432f166..7be9408308b 100644
--- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
+++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
@@ -2311,14 +2311,16 @@ public class VolumeApiServiceImpl extends ManagerBase
implements VolumeApiServic
return a.equals(b);
}
- private VolumeVO resizeVolumeInternal(VolumeVO volume, DiskOfferingVO
newDiskOffering, Long currentSize, Long newSize, Long newMinIops, Long
newMaxIops, Integer newHypervisorSnapshotReserve, boolean shrinkOk) throws
ResourceAllocationException {
+ VolumeVO resizeVolumeInternal(VolumeVO volume, DiskOfferingVO
newDiskOffering, Long currentSize, Long newSize, Long newMinIops, Long
newMaxIops, Integer newHypervisorSnapshotReserve, boolean shrinkOk) throws
ResourceAllocationException {
UserVmVO userVm = _userVmDao.findById(volume.getInstanceId());
HypervisorType hypervisorType =
_volsDao.getHypervisorType(volume.getId());
if (userVm != null) {
- if (volume.getVolumeType().equals(Volume.Type.ROOT) &&
userVm.getPowerState() != VirtualMachine.PowerState.PowerOff && hypervisorType
== HypervisorType.VMware) {
- logger.error(" For ROOT volume resize VM should be in Power
Off state.");
- throw new InvalidParameterValueException("VM current state is
: " + userVm.getPowerState() + ". But VM should be in " +
VirtualMachine.PowerState.PowerOff + " state.");
+ if (Volume.Type.ROOT.equals(volume.getVolumeType())
+ && !State.Stopped.equals(userVm.getState())
+ && HypervisorType.VMware.equals(hypervisorType)) {
+ logger.error("For ROOT volume resize VM should be in Stopped
state.");
+ throw new InvalidParameterValueException("The current VM state
is '" + userVm.getState() + "'. But the VM should be in " + State.Stopped + "
state.");
}
// serialize VM operation
AsyncJobExecutionContext jobContext =
AsyncJobExecutionContext.getCurrentExecutionContext();
@@ -2372,7 +2374,7 @@ public class VolumeApiServiceImpl extends ManagerBase
implements VolumeApiServic
shrinkOk);
}
- private void validateVolumeReadyStateAndHypervisorChecks(VolumeVO volume,
long currentSize, Long newSize) {
+ void validateVolumeReadyStateAndHypervisorChecks(VolumeVO volume, long
currentSize, Long newSize) {
// checking if there are any ongoing snapshots on the volume which is
to be resized
List<SnapshotVO> ongoingSnapshots =
_snapshotDao.listByStatus(volume.getId(), Snapshot.State.Creating,
Snapshot.State.CreatedOnPrimary, Snapshot.State.BackingUp);
if (ongoingSnapshots.size() > 0) {
@@ -2396,9 +2398,11 @@ public class VolumeApiServiceImpl extends ManagerBase
implements VolumeApiServic
UserVmVO userVm = _userVmDao.findById(volume.getInstanceId());
if (userVm != null) {
- if (volume.getVolumeType().equals(Volume.Type.ROOT) &&
userVm.getPowerState() != VirtualMachine.PowerState.PowerOff && hypervisorType
== HypervisorType.VMware) {
- logger.error(" For ROOT volume resize VM should be in Power
Off state.");
- throw new InvalidParameterValueException("VM current state is
: " + userVm.getPowerState() + ". But VM should be in " +
VirtualMachine.PowerState.PowerOff + " state.");
+ if (Volume.Type.ROOT.equals(volume.getVolumeType())
+ && !State.Stopped.equals(userVm.getState())
+ && HypervisorType.VMware.equals(hypervisorType)) {
+ logger.error("For ROOT volume resize VM should be in Stopped
state.");
+ throw new InvalidParameterValueException("The current VM state
is '" + userVm.getState() + "'. But VM should be in " + State.Stopped + "
state.");
}
}
}
@@ -2415,7 +2419,7 @@ public class VolumeApiServiceImpl extends ManagerBase
implements VolumeApiServic
}
}
- private void validateVolumeResizeWithNewDiskOfferingAndLoad(VolumeVO
volume, DiskOfferingVO existingDiskOffering, DiskOfferingVO newDiskOffering,
Long[] newSize, Long[] newMinIops, Long[] newMaxIops, Integer[]
newHypervisorSnapshotReserve) {
+ void validateVolumeResizeWithNewDiskOfferingAndLoad(VolumeVO volume,
DiskOfferingVO existingDiskOffering, DiskOfferingVO newDiskOffering, Long[]
newSize, Long[] newMinIops, Long[] newMaxIops, Integer[]
newHypervisorSnapshotReserve) {
if (newDiskOffering.getRemoved() != null) {
throw new InvalidParameterValueException("Requested disk offering
has been removed.");
}
diff --git
a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
index 77e321ab859..b806eb4afe5 100644
--- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
+++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java
@@ -33,6 +33,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -40,15 +41,6 @@ import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
-import com.cloud.event.EventTypes;
-import com.cloud.event.UsageEventUtils;
-import com.cloud.host.HostVO;
-import com.cloud.resourcelimit.CheckedReservation;
-import com.cloud.service.ServiceOfferingVO;
-import com.cloud.service.dao.ServiceOfferingDao;
-import com.cloud.vm.snapshot.VMSnapshot;
-import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
-import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.api.command.user.volume.CheckAndRepairVolumeCmd;
@@ -70,6 +62,7 @@ import
org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeAp
import org.apache.cloudstack.framework.async.AsyncCallFuture;
import org.apache.cloudstack.framework.jobs.AsyncJobExecutionContext;
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
+import org.apache.cloudstack.framework.jobs.Outcome;
import org.apache.cloudstack.framework.jobs.dao.AsyncJobJoinMapDao;
import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO;
import org.apache.cloudstack.resourcedetail.dao.SnapshotPolicyDetailsDao;
@@ -107,17 +100,23 @@ import com.cloud.dc.HostPodVO;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.HostPodDao;
+import com.cloud.event.EventTypes;
+import com.cloud.event.UsageEventUtils;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceAllocationException;
+import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.org.Grouping;
import com.cloud.projects.Project;
import com.cloud.projects.ProjectManager;
+import com.cloud.resourcelimit.CheckedReservation;
import com.cloud.serializer.GsonHelper;
import com.cloud.server.ManagementService;
import com.cloud.server.TaggedResourceService;
+import com.cloud.service.ServiceOfferingVO;
+import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.storage.Storage.ProvisioningType;
import com.cloud.storage.Volume.Type;
import com.cloud.storage.dao.DiskOfferingDao;
@@ -145,8 +144,11 @@ import com.cloud.vm.VirtualMachine.State;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
+import com.cloud.vm.snapshot.VMSnapshot;
+import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
import com.cloud.vm.snapshot.VMSnapshotVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDao;
+import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
@RunWith(MockitoJUnitRunner.class)
public class VolumeApiServiceImplTest {
@@ -2320,4 +2322,186 @@ public class VolumeApiServiceImplTest {
Mockito.doReturn(1L).when(mock2).getId();
return List.of(mock1, mock2);
}
+
+// =====================================================================
+// VMware ROOT-volume resize / offering-change: VM power_state-lag tests
+//
+// Both private guards that protect VMware ROOT-volume resize operations
+// are covered here:
+// • validateVolumeReadyStateAndHypervisorChecks (called by
changeDiskOfferingForVolumeInternal)
+// • resizeVolumeInternal (called by both resize and
change-offering flows)
+//
+// The "power_state lag" scenario: when a VMware VM is stopped via CloudStack
+// the VirtualMachine.State transitions to Stopped immediately, but the
+// VMware-side VirtualMachine.PowerState (polled from ESX) may still read
+// PoweredOn for some seconds. The production code must use only the
+// authoritative CloudStack State field and must NOT additionally gate on
+// PowerState.
+// =====================================================================
+
+ /**
+ * Positive – validateVolumeReadyStateAndHypervisorChecks:
+ * The guard must allow a VMware ROOT-volume resize when the CloudStack VM
+ * state is {@code Stopped}, regardless of the VMware power_state value.
+ * getPowerState() is intentionally left un-stubbed so that any invocation
+ * of that method would cause a Mockito strict-stubbing error and surface a
+ * regression.
+ */
+ @Test
+ public void
testValidateVolumeReadyStateVMware_VMStopped_PowerStateLag_ShouldPass()
+ throws NoSuchMethodException, InvocationTargetException,
IllegalAccessException {
+
+ long volumeId = 200L;
+ long vmId = 201L;
+
+ VolumeVO volume = Mockito.mock(VolumeVO.class);
+ when(volume.getId()).thenReturn(volumeId);
+ when(volume.getInstanceId()).thenReturn(vmId);
+ when(volume.getVolumeType()).thenReturn(Volume.Type.ROOT);
+ when(volume.getState()).thenReturn(Volume.State.Ready);
+
+ // snapshotDaoMock returns an empty list by default (Mockito default
behaviour)
+
when(volumeDaoMock.getHypervisorType(volumeId)).thenReturn(HypervisorType.VMware);
+
+ UserVmVO stoppedVm = Mockito.mock(UserVmVO.class);
+ // Authoritative cloud state: Stopped.
+ // getPowerState() is NOT stubbed – power_state lag scenario.
+ when(stoppedVm.getState()).thenReturn(State.Stopped);
+ when(userVmDaoMock.findById(vmId)).thenReturn(stoppedVm);
+
+ long currentSizeBytes = 10L << 30; // 10 GiB
+ Long newSizeBytes = 20L << 30; // 20 GiB (grow; VMware prohibits
shrink)
+
+ // Must complete without throwing any exception
+
volumeApiServiceImpl.validateVolumeReadyStateAndHypervisorChecks(volume,
currentSizeBytes, newSizeBytes);
+ }
+
+ /**
+ * Negative – validateVolumeReadyStateAndHypervisorChecks:
+ * The guard must reject a VMware ROOT-volume resize when the CloudStack VM
+ * state is {@code Running}.
+ */
+ @Test(expected = InvalidParameterValueException.class)
+ public void
testValidateVolumeReadyStateVMware_VMRunning_ShouldThrowInvalidParameterValueException()
+ throws NoSuchMethodException, IllegalAccessException {
+
+ long volumeId = 200L;
+ long vmId = 201L;
+
+ VolumeVO volume = Mockito.mock(VolumeVO.class);
+ when(volume.getId()).thenReturn(volumeId);
+ when(volume.getInstanceId()).thenReturn(vmId);
+ when(volume.getVolumeType()).thenReturn(Volume.Type.ROOT);
+ when(volume.getState()).thenReturn(Volume.State.Ready);
+
+
when(volumeDaoMock.getHypervisorType(volumeId)).thenReturn(HypervisorType.VMware);
+
+ UserVmVO runningVm = Mockito.mock(UserVmVO.class);
+ when(runningVm.getState()).thenReturn(State.Running);
+ when(userVmDaoMock.findById(vmId)).thenReturn(runningVm);
+
+ long currentSizeBytes = 10L << 30;
+ Long newSizeBytes = 20L << 30;
+
+
volumeApiServiceImpl.validateVolumeReadyStateAndHypervisorChecks(volume,
currentSizeBytes, newSizeBytes);
+ Assert.fail("Expected InvalidParameterValueException for VMware
ROOT-volume resize "
+ + "when VM state is Running");
+ }
+
+ /**
+ * Positive – resizeVolumeInternal:
+ * The VMware stopped-state guard inside {@code resizeVolumeInternal} must
NOT
+ * fire when the CloudStack VM state is {@code Stopped}, even when the
VMware
+ * power_state has not yet transitioned to PowerOff.
+ * Any exception originating from deeper plumbing (job queue, storage
layer)
+ * is acceptable; only the state-guard exception is a failure.
+ */
+ @Test
+ public void
testResizeVolumeInternal_VMware_VMStopped_PowerStateLag_ShouldNotThrowStateGuardError()
+ throws NoSuchMethodException, IllegalAccessException {
+
+ long volumeId = 300L;
+ long vmId = 301L;
+
+ VolumeVO volume = Mockito.mock(VolumeVO.class);
+ when(volume.getId()).thenReturn(volumeId);
+ when(volume.getInstanceId()).thenReturn(vmId);
+ when(volume.getVolumeType()).thenReturn(Volume.Type.ROOT);
+
+ UserVmVO stoppedVm = Mockito.mock(UserVmVO.class);
+ when(stoppedVm.getState()).thenReturn(State.Stopped); // authoritative
cloud state
+ // getPowerState() deliberately NOT stubbed – power_state lag scenario
+ when(userVmDaoMock.findById(vmId)).thenReturn(stoppedVm);
+
+
when(volumeDaoMock.getHypervisorType(volumeId)).thenReturn(HypervisorType.VMware);
+
+ // Stub the job-queue call so OutcomeImpl.s_jobMgr (static,
uninitialized in tests)
+ // is never reached. The Outcome mock returns null from get() and a
bare AsyncJobVO
+ // from getJob(), which is enough for the unmarshallResultObject path
to return null.
+ @SuppressWarnings("unchecked")
+ Outcome<Volume> outcomemock = Mockito.mock(Outcome.class);
+ AsyncJobVO stubJob = new AsyncJobVO();
+ when(outcomemock.getJob()).thenReturn(stubJob);
+
doReturn(outcomemock).when(volumeApiServiceImpl).resizeVolumeThroughJobQueue(
+ anyLong(), anyLong(), anyLong(), anyLong(),
+ nullable(Long.class), nullable(Long.class),
+ nullable(Integer.class), nullable(Long.class), anyBoolean());
+
+ // resizeVolumeInternal(VolumeVO, DiskOfferingVO, Long, Long, Long,
Long, Integer, boolean)
+ try {
+ volumeApiServiceImpl.resizeVolumeInternal(
+ volume,
+ /* newDiskOffering */ (DiskOfferingVO) null,
+ /* currentSize */ 0L,
+ /* newSize */ 1L,
+ /* newMinIops */ (Long) null,
+ /* newMaxIops */ (Long) null,
+ /* snapshotReserve */ (Integer) null,
+ /* shrinkOk */ false);
+ } catch (ResourceAllocationException e) {
+ Assert.fail("Unexpected ResourceAllocationException");
+ } catch (InvalidParameterValueException e) {
+ Assert.fail("VMware ROOT-volume resize must be allowed when
CloudStack VM state is "
+ + "Stopped, even under a power_state lag. Unexpected
exception: "
+ + e.getMessage());
+ }
+ }
+
+ /**
+ * Negative – resizeVolumeInternal:
+ * The VMware stopped-state guard inside {@code resizeVolumeInternal} must
+ * reject the operation when the CloudStack VM state is {@code Running}.
+ */
+ @Test
+ public void
testResizeVolumeInternal_VMware_VMRunning_ShouldThrowStateGuardError()
+ throws NoSuchMethodException, IllegalAccessException {
+
+ long volumeId = 300L;
+ long vmId = 301L;
+
+ VolumeVO volume = Mockito.mock(VolumeVO.class);
+ when(volume.getId()).thenReturn(volumeId);
+ when(volume.getInstanceId()).thenReturn(vmId);
+ when(volume.getVolumeType()).thenReturn(Volume.Type.ROOT);
+
+ UserVmVO runningVm = Mockito.mock(UserVmVO.class);
+ when(runningVm.getState()).thenReturn(State.Running);
+ when(userVmDaoMock.findById(vmId)).thenReturn(runningVm);
+
+
when(volumeDaoMock.getHypervisorType(volumeId)).thenReturn(HypervisorType.VMware);
+
+ try {
+ volumeApiServiceImpl.resizeVolumeInternal(
+ volume,
+ (DiskOfferingVO) null,
+ 0L, 1L, (Long) null, (Long) null, (Integer) null, false);
+ Assert.fail("Expected an InvalidParameterValueException for VMware
ROOT-volume resize when VM state is Running");
+ } catch (ResourceAllocationException e) {
+ Assert.fail("Cause must be InvalidParameterValueException, was: "
+ e.getClass());
+ } catch (InvalidParameterValueException e) {
+ Assert.assertTrue(
+ "Exception message must reference Stopped-state
requirement, was: " + e.getMessage(),
+ e.getMessage() != null && e.getMessage().contains("VM
should be in"));
+ }
+ }
}