Skip to content

Commit 678ed74

Browse files
CS-3474: Transition KVM disk-only VM snapshot to Error state on delete failure
deleteVMSnapshot in KvmFileBasedStorageVmSnapshotStrategy transitioned the snapshot to Expunging but had no exception handling, so any exception thrown by the merge/delete helpers (e.g. an agent command timeout) left the snapshot permanently stuck in Expunging. VMSnapshotManagerImpl.hasActiveVMSnapshotTasks treats Expunging as active work, so this permanently blocked reboot and further snapshot operations on the VM. Wrap the delete body in a try/catch, mirroring the existing pattern already used by takeVMSnapshot and revertVMSnapshot in the same class: on any RuntimeException, transition the snapshot to Error via the existing Expunging -> OperationFailed state machine transition before rethrowing. Test: KvmFileBasedStorageVmSnapshotStrategyTest#testDeleteVMSnapshotMarksSnapshotFailedWhenDeleteThrowsAfterExpungeRequested fails without this fix (no OperationFailed transition is ever fired) and passes with it.
1 parent 250695e commit 678ed74

2 files changed

Lines changed: 69 additions & 38 deletions

File tree

‎engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java‎

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -142,52 +142,57 @@ public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) {
142142

143143
transitStateWithoutThrow(vmSnapshotBeingDeleted, VMSnapshot.Event.ExpungeRequested);
144144

145-
List<VolumeObjectTO> volumeTOs = vmSnapshotHelper.getVolumeTOList(vmSnapshotBeingDeleted.getVmId());
146-
List<VMSnapshotVO> snapshotChildren = vmSnapshotDao.listByParentAndStateIn(vmSnapshotBeingDeleted.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden);
147-
PrimaryDataStoreTO nvramPrimaryDataStore = getPrimaryDataStoreForNvramCleanup(vmSnapshotBeingDeleted, volumeTOs);
148-
149-
long realSize = getVMSnapshotRealSize(vmSnapshotBeingDeleted);
150-
int numberOfChildren = snapshotChildren.size();
151-
152-
List<SnapshotVO> volumeSnapshotVos = new ArrayList<>();
153-
if (isCurrent && numberOfChildren == 0) {
154-
volumeSnapshotVos = mergeCurrentDeltaOnSnapshot(vmSnapshotBeingDeleted, userVm, hostId, volumeTOs);
155-
} else if (numberOfChildren == 0) {
156-
logger.debug("Deleting VM snapshot [{}] as no snapshots/volumes depend on it.", vmSnapshot.getUuid());
157-
volumeSnapshotVos = deleteSnapshot(vmSnapshotBeingDeleted, hostId);
158-
mergeOldSiblingWithOldParentIfOldParentIsDead(vmSnapshotDao.findByIdIncludingRemoved(vmSnapshotBeingDeleted.getParent()), userVm, hostId, volumeTOs);
159-
} else if (!isCurrent && numberOfChildren == 1) {
160-
VMSnapshotVO childSnapshot = snapshotChildren.get(0);
161-
volumeSnapshotVos = mergeSnapshots(vmSnapshotBeingDeleted, childSnapshot, userVm, volumeTOs, hostId);
162-
}
163-
164-
for (SnapshotVO snapshotVO : volumeSnapshotVos) {
165-
snapshotVO.setState(Snapshot.State.Destroyed);
166-
snapshotDao.update(snapshotVO.getId(), snapshotVO);
167-
}
145+
try {
146+
List<VolumeObjectTO> volumeTOs = vmSnapshotHelper.getVolumeTOList(vmSnapshotBeingDeleted.getVmId());
147+
List<VMSnapshotVO> snapshotChildren = vmSnapshotDao.listByParentAndStateIn(vmSnapshotBeingDeleted.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden);
148+
PrimaryDataStoreTO nvramPrimaryDataStore = getPrimaryDataStoreForNvramCleanup(vmSnapshotBeingDeleted, volumeTOs);
149+
150+
long realSize = getVMSnapshotRealSize(vmSnapshotBeingDeleted);
151+
int numberOfChildren = snapshotChildren.size();
152+
153+
List<SnapshotVO> volumeSnapshotVos = new ArrayList<>();
154+
if (isCurrent && numberOfChildren == 0) {
155+
volumeSnapshotVos = mergeCurrentDeltaOnSnapshot(vmSnapshotBeingDeleted, userVm, hostId, volumeTOs);
156+
} else if (numberOfChildren == 0) {
157+
logger.debug("Deleting VM snapshot [{}] as no snapshots/volumes depend on it.", vmSnapshot.getUuid());
158+
volumeSnapshotVos = deleteSnapshot(vmSnapshotBeingDeleted, hostId);
159+
mergeOldSiblingWithOldParentIfOldParentIsDead(vmSnapshotDao.findByIdIncludingRemoved(vmSnapshotBeingDeleted.getParent()), userVm, hostId, volumeTOs);
160+
} else if (!isCurrent && numberOfChildren == 1) {
161+
VMSnapshotVO childSnapshot = snapshotChildren.get(0);
162+
volumeSnapshotVos = mergeSnapshots(vmSnapshotBeingDeleted, childSnapshot, userVm, volumeTOs, hostId);
163+
}
168164

169-
for (VolumeObjectTO volumeTo : volumeTOs) {
170-
publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_DELETE, vmSnapshotBeingDeleted, userVm, volumeTo);
171-
virtualSize += volumeTo.getSize();
172-
}
165+
for (SnapshotVO snapshotVO : volumeSnapshotVos) {
166+
snapshotVO.setState(Snapshot.State.Destroyed);
167+
snapshotDao.update(snapshotVO.getId(), snapshotVO);
168+
}
173169

174-
publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_OFF_PRIMARY, vmSnapshotBeingDeleted, userVm, realSize, virtualSize);
170+
for (VolumeObjectTO volumeTo : volumeTOs) {
171+
publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_DELETE, vmSnapshotBeingDeleted, userVm, volumeTo);
172+
virtualSize += volumeTo.getSize();
173+
}
175174

176-
if (numberOfChildren > 1 || (isCurrent && numberOfChildren == 1)) {
177-
transitStateWithoutThrow(vmSnapshotBeingDeleted, VMSnapshot.Event.Hide);
178-
return true;
179-
}
175+
publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_OFF_PRIMARY, vmSnapshotBeingDeleted, userVm, realSize, virtualSize);
180176

181-
deleteNvramSnapshotIfNeeded(vmSnapshotBeingDeleted, hostId, nvramPrimaryDataStore);
177+
if (numberOfChildren > 1 || (isCurrent && numberOfChildren == 1)) {
178+
transitStateWithoutThrow(vmSnapshotBeingDeleted, VMSnapshot.Event.Hide);
179+
return true;
180+
}
182181

183-
transitStateWithoutThrow(vmSnapshotBeingDeleted, VMSnapshot.Event.OperationSucceeded);
182+
deleteNvramSnapshotIfNeeded(vmSnapshotBeingDeleted, hostId, nvramPrimaryDataStore);
184183

185-
vmSnapshotDetailsDao.removeDetails(vmSnapshotBeingDeleted.getId());
184+
transitStateWithoutThrow(vmSnapshotBeingDeleted, VMSnapshot.Event.OperationSucceeded);
186185

187-
vmSnapshotBeingDeleted.setRemoved(DateUtil.now());
188-
vmSnapshotDao.update(vmSnapshotBeingDeleted.getId(), vmSnapshotBeingDeleted);
186+
vmSnapshotDetailsDao.removeDetails(vmSnapshotBeingDeleted.getId());
189187

190-
return true;
188+
vmSnapshotBeingDeleted.setRemoved(DateUtil.now());
189+
vmSnapshotDao.update(vmSnapshotBeingDeleted.getId(), vmSnapshotBeingDeleted);
190+
191+
return true;
192+
} catch (RuntimeException ex) {
193+
transitStateWithoutThrow(vmSnapshotBeingDeleted, VMSnapshot.Event.OperationFailed);
194+
throw ex;
195+
}
191196
}
192197

193198
@Override

‎engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategyTest.java‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,32 @@ public void testDeleteVMSnapshotFailsWhenHostLacksNvramAwareCleanupCapabilityFor
439439
strategy.deleteVMSnapshot(vmSnapshot);
440440
}
441441

442+
@Test(expected = CloudRuntimeException.class)
443+
public void testDeleteVMSnapshotMarksSnapshotFailedWhenDeleteThrowsAfterExpungeRequested() throws Exception {
444+
long vmId = 10L;
445+
long vmSnapshotId = 20L;
446+
long hostId = 40L;
447+
448+
UserVmVO userVm = mock(UserVmVO.class);
449+
VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class);
450+
451+
when(vmSnapshot.getVmId()).thenReturn(vmId);
452+
when(vmSnapshot.getId()).thenReturn(vmSnapshotId);
453+
when(vmSnapshot.getUuid()).thenReturn("vm-snapshot");
454+
when(userVm.getState()).thenReturn(VirtualMachine.State.Running);
455+
when(strategy.userVmDao.findById(vmId)).thenReturn(userVm);
456+
when(vmSnapshotHelper.pickRunningHost(vmId)).thenReturn(hostId);
457+
when(vmSnapshotHelper.getVolumeTOList(vmId)).thenThrow(new CloudRuntimeException("Communication failure with host, command timed out"));
458+
459+
try {
460+
strategy.deleteVMSnapshot(vmSnapshot);
461+
} finally {
462+
InOrder inOrder = inOrder(vmSnapshotHelper);
463+
inOrder.verify(vmSnapshotHelper).vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.ExpungeRequested);
464+
inOrder.verify(vmSnapshotHelper).vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationFailed);
465+
}
466+
}
467+
442468
@Test(expected = CloudRuntimeException.class)
443469
public void testTakeVmSnapshotInternalFailsWhenHostLacksUefiCapabilityForUefiVm() throws Exception {
444470
long vmId = 10L;

0 commit comments

Comments
 (0)