Skip to content

Commit dbb2c6e

Browse files
Refuse snapshot operations cleanly when the volume is gone
revertSnapshot looked up the snapshot's volume with findById(), which does not return removed volumes, and used it without a null check. Any snapshot of a deleted volume therefore failed with an NPE on VolumeVO.getState(). It now fails with an error saying the volume no longer exists. The same pattern elsewhere: - copySnapshot without sourcezoneid took the source zone from the volume, found the same way. For a deleted volume it now falls back to the snapshot's own zone, which is the zone it was taken in. - createVolume and createTemplate from a snapshot look the volume up including removed rows, but still failed with an NPE when the row itself is missing. They now refuse with a clear error. Fixes #14644 Signed-off-by: Brad House <bhouse@nexthop.ai>
1 parent d02adcd commit dbb2c6e

7 files changed

Lines changed: 92 additions & 6 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,9 @@ public VolumeVO allocVolume(CreateVolumeCmd cmd) throws ResourceAllocationExcept
901901
}
902902

903903
parentVolume = _volsDao.findByIdIncludingRemoved(snapshotCheck.getVolumeId());
904+
if (parentVolume == null) {
905+
throw new InvalidParameterValueException(String.format("Cannot create a volume from snapshot %s, because the volume it was taken from no longer exists", snapshotCheck.getUuid()));
906+
}
904907

905908
// Don't support creating templates from encrypted volumes (yet)
906909
if (parentVolume.getPassphraseId() != null) {

‎server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,9 @@ public Snapshot revertSnapshot(Long snapshotId) {
377377
throw new InvalidParameterValueException(String.format("The snapshot [%s] is part of a [%s] snapshots and cannot be reverted separately", snapshot, snapshot.getTypeDescription()));
378378
}
379379
VolumeVO volume = _volsDao.findById(snapshot.getVolumeId());
380+
if (volume == null) {
381+
throw new InvalidParameterValueException(String.format("Cannot revert snapshot %s, because its volume no longer exists", snapshot.getUuid()));
382+
}
380383
if (volume.getState() != Volume.State.Ready) {
381384
throw new InvalidParameterValueException("The volume is not in Ready state.");
382385
}
@@ -2265,9 +2268,10 @@ protected Pair<SnapshotVO, Long> getCheckedSnapshotForCopy(final SnapshotVO snap
22652268
if (snapshot.getLocationType() != null && !Snapshot.LocationType.SECONDARY.equals(snapshot.getLocationType()) && !useStorageReplication) {
22662269
throw new InvalidParameterValueException("Snapshot is not backed up");
22672270
}
2268-
Volume volume = _volsDao.findById(snapshot.getVolumeId());
22692271
if (sourceZoneId == null) {
2270-
sourceZoneId = volume.getDataCenterId();
2272+
// The volume may have been removed since the snapshot was taken; the snapshot records the same zone.
2273+
Volume volume = _volsDao.findById(snapshot.getVolumeId());
2274+
sourceZoneId = volume != null ? volume.getDataCenterId() : snapshot.getDataCenterId();
22712275
}
22722276
if (CollectionUtils.isEmpty(destZoneIds)) {
22732277
throw new InvalidParameterValueException("Please specify valid destination zone(s).");

‎server/src/main/java/com/cloud/template/TemplateManagerImpl.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2006,9 +2006,12 @@ public VMTemplateVO createPrivateTemplateRecord(CreateTemplateCmd cmd, Account t
20062006
}
20072007
// Volume could be removed so find including removed to record source template id.
20082008
volume = _volumeDao.findByIdIncludingRemoved(snapshot.getVolumeId());
2009+
if (volume == null) {
2010+
throw new InvalidParameterValueException(String.format("Cannot create a Template from Snapshot %s, because the volume it was taken from no longer exists", snapshot.getUuid()));
2011+
}
20092012

20102013
// Don't support creating templates from encrypted volumes (yet)
2011-
if (volume != null && volume.getPassphraseId() != null) {
2014+
if (volume.getPassphraseId() != null) {
20122015
throw new UnsupportedOperationException("Cannot create Templates from Snapshots of encrypted volumes");
20132016
}
20142017

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
7171
import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
7272
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
73+
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
7374
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
7475
import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao;
7576
import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO;
@@ -255,6 +256,9 @@ public class VolumeApiServiceImplTest {
255256
@Mock
256257
private SnapshotDao snapshotDaoMock;
257258

259+
@Mock
260+
private SnapshotDataStoreDao snapshotDataStoreDaoMock;
261+
258262
@Mock
259263
private SnapshotPolicyDetailsDao snapshotPolicyDetailsDao;
260264

@@ -625,6 +629,26 @@ public void testNonEmptyGetVolumeNameFromCmd() {
625629
Assert.assertSame(volumeApiServiceImpl.getVolumeNameFromCommand(createVol), "abc");
626630
}
627631

632+
@Test
633+
public void allocVolumeFromSnapshotWhoseVolumeRowIsGoneIsRefused() throws ResourceAllocationException {
634+
long snapshotId = 42L;
635+
SnapshotVO snapshot = Mockito.mock(SnapshotVO.class);
636+
Mockito.when(snapshot.getState()).thenReturn(Snapshot.State.BackedUp);
637+
Mockito.when(snapshot.getVolumeId()).thenReturn(volumeMockId);
638+
Mockito.when(snapshotDaoMock.findById(snapshotId)).thenReturn(snapshot);
639+
Mockito.when(volumeDaoMock.findByIdIncludingRemoved(volumeMockId)).thenReturn(null);
640+
Mockito.when(createVol.getDisplayVolume()).thenReturn(null);
641+
Mockito.when(createVol.getDiskOfferingId()).thenReturn(null);
642+
Mockito.when(createVol.getSnapshotId()).thenReturn(snapshotId);
643+
644+
try {
645+
volumeApiServiceImpl.allocVolume(createVol);
646+
Assert.fail("Creating a volume from a snapshot whose volume row is gone must be refused");
647+
} catch (InvalidParameterValueException e) {
648+
Assert.assertTrue(e.getMessage(), e.getMessage().contains("no longer exists"));
649+
}
650+
}
651+
628652
@Test
629653
public void testUpdateMissingRootDiskControllerWithNullChainInfo() {
630654
volumeApiServiceImpl.updateMissingRootDiskController(null, null);

‎server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerImplTest.java‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,11 +447,22 @@ public void testGetCheckedSnapshotForCopyDestNotSpecified() {
447447
public void testGetCheckedSnapshotForCopyDestContainsSource() {
448448
SnapshotVO snapshotVO = Mockito.mock(SnapshotVO.class);
449449
Mockito.when(snapshotVO.getState()).thenReturn(Snapshot.State.BackedUp);
450-
Mockito.when(snapshotVO.getVolumeId()).thenReturn(1L);
451-
Mockito.when(volumeDao.findById(Mockito.anyLong())).thenReturn(Mockito.mock(VolumeVO.class));
452450
snapshotManager.getCheckedSnapshotForCopy(snapshotVO, List.of(100L, 1L), 1L, false);
453451
}
454452

453+
@Test
454+
public void testGetCheckedSnapshotForCopyVolumeRemoved() {
455+
final Long zoneId = 1L;
456+
SnapshotVO snapshotVO = Mockito.mock(SnapshotVO.class);
457+
Mockito.when(snapshotVO.getState()).thenReturn(Snapshot.State.BackedUp);
458+
Mockito.when(snapshotVO.getVolumeId()).thenReturn(1L);
459+
Mockito.when(snapshotVO.getDataCenterId()).thenReturn(zoneId);
460+
Mockito.when(volumeDao.findById(1L)).thenReturn(null);
461+
Mockito.when(dataCenterDao.findById(zoneId)).thenReturn(Mockito.mock(DataCenterVO.class));
462+
Pair<SnapshotVO, Long> result = snapshotManager.getCheckedSnapshotForCopy(snapshotVO, List.of(100L, 101L), null, false);
463+
Assert.assertEquals(zoneId, result.second());
464+
}
465+
455466
@Test(expected = InvalidParameterValueException.class)
456467
public void testGetCheckedSnapshotForCopyNullSourceZone() {
457468
SnapshotVO snapshotVO = Mockito.mock(SnapshotVO.class);

‎server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,19 @@ public void testRevertSnapshotF1() {
344344
_snapshotMgr.revertSnapshot(TEST_SNAPSHOT_ID);
345345
}
346346

347+
// volume removed or its row gone
348+
@Test
349+
public void testRevertSnapshotVolumeGone() {
350+
when(_volumeDao.findById(anyLong())).thenReturn(null);
351+
try {
352+
_snapshotMgr.revertSnapshot(TEST_SNAPSHOT_ID);
353+
Assert.fail("Reverting a snapshot whose volume no longer exists must be refused");
354+
} catch (InvalidParameterValueException e) {
355+
Assert.assertTrue(e.getMessage(), e.getMessage().contains("no longer exists"));
356+
}
357+
Mockito.verify(snapshotStrategy, Mockito.never()).revertSnapshot(any());
358+
}
359+
347360
// vm on Xenserver, return null
348361
@Test
349362
public void testRevertSnapshotF2() {

‎server/src/test/java/com/cloud/template/TemplateManagerImplTest.java‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
import org.springframework.core.type.filter.TypeFilter;
102102

103103
import com.cloud.agent.AgentManager;
104+
import com.cloud.api.ApiDBUtils;
104105
import com.cloud.api.query.dao.SnapshotJoinDao;
105106
import com.cloud.api.query.dao.UserVmJoinDao;
106107
import com.cloud.dc.dao.DataCenterDao;
@@ -499,6 +500,11 @@ public void testCreatePrivateTemplateRecordForRegionStore() throws ResourceAlloc
499500
when(mockSnapshot.getVolumeId()).thenReturn(1L);
500501
when(mockSnapshot.getState()).thenReturn(Snapshot.State.BackedUp);
501502
when(mockSnapshot.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.XenServer);
503+
VolumeVO mockVolume = mock(VolumeVO.class);
504+
when(mockVolume.getPassphraseId()).thenReturn(null);
505+
when(mockVolume.getTemplateId()).thenReturn(null);
506+
when(mockVolume.getInstanceId()).thenReturn(null);
507+
when(volumeDao.findByIdIncludingRemoved(1L)).thenReturn(mockVolume);
502508

503509
GuestOSVO mockGuestOS = mock(GuestOSVO.class);
504510
when(guestOSDao.findById(anyLong())).thenReturn(mockGuestOS);
@@ -518,12 +524,34 @@ public VMTemplateVO answer(InvocationOnMock invocationOnMock) throws Throwable {
518524
}
519525
});
520526

521-
try (MockedConstruction<CheckedReservation> mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) {
527+
try (MockedConstruction<CheckedReservation> mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class);
528+
MockedStatic<ApiDBUtils> apiDBUtilsMock = Mockito.mockStatic(ApiDBUtils.class)) {
522529
VMTemplateVO template = templateManager.createPrivateTemplateRecord(mockCreateCmd, mockTemplateOwner);
523530
assertTrue("Template in a region store should have cross zones set", template.isCrossZones());
524531
}
525532
}
526533

534+
@Test
535+
public void testCreatePrivateTemplateRecordSnapshotVolumeRowGone() throws ResourceAllocationException {
536+
CreateTemplateCmd mockCreateCmd = mock(CreateTemplateCmd.class);
537+
when(mockCreateCmd.getTemplateName()).thenReturn("test");
538+
when(mockCreateCmd.getVolumeId()).thenReturn(null);
539+
when(mockCreateCmd.getSnapshotId()).thenReturn(1L);
540+
541+
SnapshotVO mockSnapshot = mock(SnapshotVO.class);
542+
when(snapshotDao.findById(1L)).thenReturn(mockSnapshot);
543+
when(mockSnapshot.getVolumeId()).thenReturn(2L);
544+
when(volumeDao.findByIdIncludingRemoved(2L)).thenReturn(null);
545+
546+
try {
547+
templateManager.createPrivateTemplateRecord(mockCreateCmd, mock(Account.class));
548+
fail("Creating a Template from a Snapshot whose volume row is gone must be refused");
549+
} catch (InvalidParameterValueException e) {
550+
assertTrue(e.getMessage(), e.getMessage().contains("no longer exists"));
551+
}
552+
Mockito.verify(vmTemplateDao, Mockito.never()).persist(any(VMTemplateVO.class));
553+
}
554+
527555
@Test
528556
public void testLinkUserDataToTemplate() {
529557
LinkUserDataToTemplateCmd cmd = Mockito.mock(LinkUserDataToTemplateCmd.class);

0 commit comments

Comments
 (0)