Skip to content

Commit c18ebe2

Browse files
committed
kvm: fix delete of failed Instance Snapshot on stopped VM with RBD storage
StorageVMSnapshotStrategy.canHandle() required either State.Allocated or a persisted vm_snapshot_details/STORAGE_SNAPSHOT row before it would claim a VM snapshot. A snapshot that fails before any underlying disk snapshot is taken (e.g. VM freeze fails because the QEMU guest agent is not connected) transitions to State.Error without ever persisting such a row, so canHandle() returned CANT_HANDLE and deletion fell back to DefaultVMSnapshotStrategy. That strategy sends a DeleteVMSnapshotCommand to the KVM agent, which requires a live libvirt domain - failing with 'Domain not found' once the VM is stopped, and blocking both snapshot deletion and VM destruction. Let StorageVMSnapshotStrategy also claim ownership of snapshots in State.Error even when no disk-snapshot details were persisted: there is nothing on the hypervisor to clean up in that case, and deleteVMSnapshot()/deleteDiskSnapshot() are already safe no-ops when the details list is empty - no libvirt domain is required. Fixes #13972
1 parent a7697fe commit c18ebe2

2 files changed

Lines changed: 43 additions & 1 deletion

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,9 @@ public boolean revertVMSnapshot(VMSnapshot vmSnapshot) {
338338
@Override
339339
public StrategyPriority canHandle(VMSnapshot vmSnapshot) {
340340
UserVmVO userVm = userVmDao.findById(vmSnapshot.getVmId());
341-
if (!VMSnapshot.State.Allocated.equals(vmSnapshot.getState())) {
341+
// A snapshot that failed before any disk snapshot was taken (e.g. freeze failed) is in Error state
342+
// without STORAGE_SNAPSHOT details. Still handle it, as the default strategy needs a libvirt domain.
343+
if (!VMSnapshot.State.Allocated.equals(vmSnapshot.getState()) && !VMSnapshot.State.Error.equals(vmSnapshot.getState())) {
342344
List<VMSnapshotDetailsVO> vmSnapshotDetails = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), STORAGE_SNAPSHOT);
343345
if (CollectionUtils.isEmpty(vmSnapshotDetails)) {
344346
return StrategyPriority.CANT_HANDLE;

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy;
3838
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy.SnapshotOperation;
3939
import org.apache.cloudstack.engine.subsystem.api.storage.StorageStrategyFactory;
40+
import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority;
4041
import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotOptions;
4142
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
4243
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
@@ -297,6 +298,45 @@ public void testDeleteVMsnapshot() throws AgentUnavailableException, OperationTi
297298
assertTrue(result);
298299
}
299300

301+
@Test
302+
public void testCanHandleErrorStateWithNoDiskSnapshots() {
303+
// Simulates a failed Instance Snapshot (e.g. freeze failed before any disk snapshot was taken):
304+
// no vm_snapshot_details/STORAGE_SNAPSHOT rows exist, but the strategy must still claim it so
305+
// delete/cleanup does not fall back to DefaultVMSnapshotStrategy (which needs a live libvirt domain).
306+
VMSnapshotVO vmSnapshot = Mockito.mock(VMSnapshotVO.class);
307+
Long vmId = 1L;
308+
UserVmVO userVmVO = Mockito.mock(UserVmVO.class);
309+
Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId);
310+
Mockito.when(vmSnapshot.getId()).thenReturn(1L);
311+
Mockito.when(vmSnapshot.getState()).thenReturn(VMSnapshot.State.Error);
312+
Mockito.when(vmSnapshot.getType()).thenReturn(VMSnapshot.Type.Disk);
313+
Mockito.when(userVmDao.findById(vmId)).thenReturn(userVmVO);
314+
Mockito.when(userVmVO.getHypervisorType()).thenReturn(HypervisorType.KVM);
315+
Mockito.when(vmSnapshotDetailsDao.findDetails(ArgumentMatchers.anyLong(), ArgumentMatchers.anyString()))
316+
.thenReturn(new ArrayList<>());
317+
318+
StrategyPriority priority = vmStrategy.canHandle(vmSnapshot);
319+
assertEquals(StrategyPriority.HYPERVISOR, priority);
320+
}
321+
322+
@Test
323+
public void testCanHandleReadyStateWithNoDiskSnapshotsCantHandle() {
324+
// A Ready snapshot with no persisted disk snapshots is inconsistent state, unrelated to the
325+
// Error-before-any-snapshot scenario above, so the existing CANT_HANDLE behaviour is preserved.
326+
VMSnapshotVO vmSnapshot = Mockito.mock(VMSnapshotVO.class);
327+
Long vmId = 1L;
328+
UserVmVO userVmVO = Mockito.mock(UserVmVO.class);
329+
Mockito.when(vmSnapshot.getVmId()).thenReturn(vmId);
330+
Mockito.when(vmSnapshot.getId()).thenReturn(1L);
331+
Mockito.when(vmSnapshot.getState()).thenReturn(VMSnapshot.State.Ready);
332+
Mockito.when(userVmDao.findById(vmId)).thenReturn(userVmVO);
333+
Mockito.when(vmSnapshotDetailsDao.findDetails(ArgumentMatchers.anyLong(), ArgumentMatchers.anyString()))
334+
.thenReturn(new ArrayList<>());
335+
336+
StrategyPriority priority = vmStrategy.canHandle(vmSnapshot);
337+
assertEquals(StrategyPriority.CANT_HANDLE, priority);
338+
}
339+
300340
@SuppressWarnings("unchecked")
301341
private SnapshotVO testFindSnapshotByName(String snapshotName) {
302342
SearchBuilder<SnapshotVO> sb = Mockito.mock(SearchBuilder.class);

0 commit comments

Comments
 (0)