Skip to content

Commit e580af8

Browse files
committed
Clean up primary-only snapshot DB records during volume deletion to prevent orphaned entries
1 parent 7048944 commit e580af8

File tree

2 files changed

+93
-0
lines changed

2 files changed

+93
-0
lines changed

server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,7 @@ public boolean deleteVolume(long volumeId, Account caller) throws ConcurrentOper
16401640

16411641
private boolean deleteVolumeFromStorage(VolumeVO volume, Account caller) throws ConcurrentOperationException {
16421642
try {
1643+
cleanupSnapshotRecordsInPrimaryStorageOnly(volume);
16431644
expungeVolumesInPrimaryStorageIfNeeded(volume);
16441645
expungeVolumesInSecondaryStorageIfNeeded(volume);
16451646
cleanVolumesCache(volume);
@@ -1650,6 +1651,34 @@ private boolean deleteVolumeFromStorage(VolumeVO volume, Account caller) throws
16501651
}
16511652
}
16521653

1654+
/**
1655+
* Cleans up snapshot DB records for snapshots that exist only on primary storage (no secondary copy).
1656+
* This handles the case where snapshot.backup.to.secondary=false and the volume
1657+
* is being deleted during VM expunge; the RBD snapshots will be destroyed along with the image,
1658+
* so the DB records need to be cleaned up to avoid orphaned entries.
1659+
*/
1660+
protected void cleanupSnapshotRecordsInPrimaryStorageOnly(VolumeVO volume) {
1661+
List<SnapshotVO> snapshots = _snapshotDao.listByVolumeId(volume.getId());
1662+
if (CollectionUtils.isEmpty(snapshots)) {
1663+
return;
1664+
}
1665+
for (SnapshotVO snapshot : snapshots) {
1666+
if (Snapshot.State.Destroyed.equals(snapshot.getState())) {
1667+
continue;
1668+
}
1669+
List<SnapshotDataStoreVO> primaryRefs = _snapshotDataStoreDao.listBySnapshotAndDataStoreRole(snapshot.getId(), DataStoreRole.Primary);
1670+
List<SnapshotDataStoreVO> secondaryRefs = _snapshotDataStoreDao.listBySnapshotAndDataStoreRole(snapshot.getId(), DataStoreRole.Image);
1671+
if (CollectionUtils.isNotEmpty(primaryRefs) && CollectionUtils.isEmpty(secondaryRefs)) {
1672+
logger.info("Cleaning up snapshot {} (primary-only, no secondary copy) as volume {} is being deleted", snapshot, volume);
1673+
for (SnapshotDataStoreVO ref : primaryRefs) {
1674+
_snapshotDataStoreDao.expunge(ref.getId());
1675+
}
1676+
snapshot.setState(Snapshot.State.Destroyed);
1677+
_snapshotDao.update(snapshot.getId(), snapshot);
1678+
}
1679+
}
1680+
}
1681+
16531682
/**
16541683
* Retrieves and validates the volume for the {@link #deleteVolume(long, Account)} method. The following validation are executed.
16551684
* <ul>

server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@
122122
import com.cloud.storage.Volume.Type;
123123
import com.cloud.storage.dao.DiskOfferingDao;
124124
import com.cloud.storage.dao.SnapshotDao;
125+
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
126+
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO;
125127
import com.cloud.storage.dao.StoragePoolTagsDao;
126128
import com.cloud.storage.dao.VMTemplateDao;
127129
import com.cloud.storage.dao.VolumeDao;
@@ -253,6 +255,9 @@ public class VolumeApiServiceImplTest {
253255
@Mock
254256
private SnapshotDao snapshotDaoMock;
255257

258+
@Mock
259+
private SnapshotDataStoreDao snapshotDataStoreDaoMock;
260+
256261
@Mock
257262
private SnapshotPolicyDetailsDao snapshotPolicyDetailsDao;
258263

@@ -2288,4 +2293,63 @@ private List<VMSnapshotVO> generateVmSnapshotVoList(VMSnapshot.Type t1, VMSnapsh
22882293
Mockito.doReturn(1L).when(mock2).getId();
22892294
return List.of(mock1, mock2);
22902295
}
2296+
2297+
@Test
2298+
public void testCleanupSnapshotRecordsInPrimaryStorageOnly() {
2299+
VolumeVO volume = Mockito.mock(VolumeVO.class);
2300+
Mockito.when(volume.getId()).thenReturn(1L);
2301+
2302+
SnapshotVO snapshot = Mockito.mock(SnapshotVO.class);
2303+
Mockito.when(snapshot.getId()).thenReturn(10L);
2304+
Mockito.when(snapshot.getState()).thenReturn(Snapshot.State.BackedUp);
2305+
Mockito.when(snapshotDaoMock.listByVolumeId(1L)).thenReturn(List.of(snapshot));
2306+
2307+
SnapshotDataStoreVO primaryRef = Mockito.mock(SnapshotDataStoreVO.class);
2308+
Mockito.when(primaryRef.getId()).thenReturn(100L);
2309+
Mockito.when(snapshotDataStoreDaoMock.listBySnapshotAndDataStoreRole(10L, DataStoreRole.Primary)).thenReturn(List.of(primaryRef));
2310+
Mockito.when(snapshotDataStoreDaoMock.listBySnapshotAndDataStoreRole(10L, DataStoreRole.Image)).thenReturn(Collections.emptyList());
2311+
2312+
volumeApiServiceImpl.cleanupSnapshotRecordsInPrimaryStorageOnly(volume);
2313+
2314+
Mockito.verify(snapshotDataStoreDaoMock).expunge(100L);
2315+
Mockito.verify(snapshot).setState(Snapshot.State.Destroyed);
2316+
Mockito.verify(snapshotDaoMock).update(10L, snapshot);
2317+
}
2318+
2319+
@Test
2320+
public void testCleanupSnapshotRecordsInPrimaryStorageOnlySkipsWhenSecondaryExists() {
2321+
VolumeVO volume = Mockito.mock(VolumeVO.class);
2322+
Mockito.when(volume.getId()).thenReturn(1L);
2323+
2324+
SnapshotVO snapshot = Mockito.mock(SnapshotVO.class);
2325+
Mockito.when(snapshot.getId()).thenReturn(10L);
2326+
Mockito.when(snapshot.getState()).thenReturn(Snapshot.State.BackedUp);
2327+
Mockito.when(snapshotDaoMock.listByVolumeId(1L)).thenReturn(List.of(snapshot));
2328+
2329+
SnapshotDataStoreVO primaryRef = Mockito.mock(SnapshotDataStoreVO.class);
2330+
SnapshotDataStoreVO secondaryRef = Mockito.mock(SnapshotDataStoreVO.class);
2331+
Mockito.when(snapshotDataStoreDaoMock.listBySnapshotAndDataStoreRole(10L, DataStoreRole.Primary)).thenReturn(List.of(primaryRef));
2332+
Mockito.when(snapshotDataStoreDaoMock.listBySnapshotAndDataStoreRole(10L, DataStoreRole.Image)).thenReturn(List.of(secondaryRef));
2333+
2334+
volumeApiServiceImpl.cleanupSnapshotRecordsInPrimaryStorageOnly(volume);
2335+
2336+
Mockito.verify(snapshotDataStoreDaoMock, Mockito.never()).expunge(Mockito.anyLong());
2337+
Mockito.verify(snapshotDaoMock, Mockito.never()).update(Mockito.anyLong(), Mockito.any(SnapshotVO.class));
2338+
}
2339+
2340+
@Test
2341+
public void testCleanupSnapshotRecordsInPrimaryStorageOnlySkipsDestroyedSnapshots() {
2342+
VolumeVO volume = Mockito.mock(VolumeVO.class);
2343+
Mockito.when(volume.getId()).thenReturn(1L);
2344+
2345+
SnapshotVO snapshot = Mockito.mock(SnapshotVO.class);
2346+
Mockito.when(snapshot.getId()).thenReturn(10L);
2347+
Mockito.when(snapshot.getState()).thenReturn(Snapshot.State.Destroyed);
2348+
Mockito.when(snapshotDaoMock.listByVolumeId(1L)).thenReturn(List.of(snapshot));
2349+
2350+
volumeApiServiceImpl.cleanupSnapshotRecordsInPrimaryStorageOnly(volume);
2351+
2352+
Mockito.verify(snapshotDataStoreDaoMock, Mockito.never()).listBySnapshotAndDataStoreRole(Mockito.anyLong(), Mockito.any());
2353+
Mockito.verify(snapshotDataStoreDaoMock, Mockito.never()).expunge(Mockito.anyLong());
2354+
}
22912355
}

0 commit comments

Comments
 (0)