Skip to content

Commit b229715

Browse files
snapshot: reject a duplicate snapshot name for a volume
Two snapshots of the same volume with the same name map to the same file on the snapshot store, so creating the second overwrites the first, and later deleting either one removes the shared file and leaves the other snapshot pointing at nothing. Reject creating a snapshot when an active (non-destroyed) snapshot with the same name already exists for the volume. Auto-generated names already carry a timestamp so they are unaffected. Fixes: #13051
1 parent 3a79799 commit b229715

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1721,6 +1721,12 @@ public Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName,
17211721
if (snapshotName == null)
17221722
snapshotName = vmDisplayName + "_" + volume.getName() + "_" + timeString;
17231723

1724+
for (SnapshotVO existingSnapshot : _snapshotDao.listByStatusNotIn(volumeId, Snapshot.State.Destroyed, Snapshot.State.Error)) {
1725+
if (snapshotName.equals(existingSnapshot.getName())) {
1726+
throw new InvalidParameterValueException(String.format("A snapshot with name [%s] already exists for volume %s.", snapshotName, volume));
1727+
}
1728+
}
1729+
17241730
HypervisorType hypervisorType = HypervisorType.None;
17251731
StoragePoolVO storagePool = _storagePoolDao.findById(volume.getDataStore().getId());
17261732
if (storagePool.getScope() == ScopeType.ZONE) {

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,13 @@
2828
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotResult;
2929
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService;
3030
import org.apache.cloudstack.framework.async.AsyncCallFuture;
31+
import org.apache.cloudstack.context.CallContext;
32+
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
33+
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
34+
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
3135
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
3236
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO;
37+
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
3338
import org.junit.Assert;
3439
import org.junit.Test;
3540
import org.junit.runner.RunWith;
@@ -45,10 +50,12 @@
4550
import com.cloud.dc.dao.DataCenterDao;
4651
import com.cloud.event.ActionEventUtils;
4752
import com.cloud.exception.InvalidParameterValueException;
53+
import com.cloud.hypervisor.Hypervisor.HypervisorType;
4854
import com.cloud.exception.PermissionDeniedException;
4955
import com.cloud.exception.ResourceUnavailableException;
5056
import com.cloud.org.Grouping;
5157
import com.cloud.storage.DataStoreRole;
58+
import com.cloud.storage.ScopeType;
5259
import com.cloud.storage.Snapshot;
5360
import com.cloud.storage.SnapshotVO;
5461
import com.cloud.storage.VolumeVO;
@@ -59,8 +66,10 @@
5966
import com.cloud.user.AccountManager;
6067
import com.cloud.user.AccountVO;
6168
import com.cloud.user.ResourceLimitService;
69+
import com.cloud.user.User;
6270
import com.cloud.user.dao.AccountDao;
6371
import com.cloud.utils.Pair;
72+
import com.cloud.vm.dao.UserVmDao;
6473

6574
@RunWith(MockitoJUnitRunner.class)
6675
public class SnapshotManagerImplTest {
@@ -86,9 +95,44 @@ public class SnapshotManagerImplTest {
8695
SnapshotZoneDao snapshotZoneDao;
8796
@Mock
8897
VolumeDao volumeDao;
98+
@Mock
99+
PrimaryDataStoreDao primaryDataStoreDao;
100+
@Mock
101+
VolumeDataFactory volFactory;
102+
@Mock
103+
UserVmDao userVmDao;
89104
@InjectMocks
90105
SnapshotManagerImpl snapshotManager = new SnapshotManagerImpl();
91106

107+
@Test
108+
public void testAllocSnapshotRejectsDuplicateNameForVolume() {
109+
long volumeId = 1L;
110+
CallContext.register(Mockito.mock(User.class), Mockito.mock(Account.class));
111+
try {
112+
VolumeInfo volume = Mockito.mock(VolumeInfo.class);
113+
Mockito.when(volFactory.getVolume(volumeId)).thenReturn(volume);
114+
DataStore dataStore = Mockito.mock(DataStore.class);
115+
Mockito.when(volume.getDataStore()).thenReturn(dataStore);
116+
Mockito.when(dataStore.getId()).thenReturn(10L);
117+
StoragePoolVO pool = Mockito.mock(StoragePoolVO.class);
118+
Mockito.when(primaryDataStoreDao.findById(10L)).thenReturn(pool);
119+
Mockito.when(pool.getScope()).thenReturn(ScopeType.ZONE);
120+
Mockito.when(pool.getHypervisor()).thenReturn(HypervisorType.None);
121+
Mockito.when(volume.getInstanceId()).thenReturn(null);
122+
Mockito.when(volume.getAccountId()).thenReturn(2L);
123+
124+
SnapshotVO existing = Mockito.mock(SnapshotVO.class);
125+
Mockito.when(existing.getName()).thenReturn("dup");
126+
Mockito.when(snapshotDao.listByStatusNotIn(volumeId, Snapshot.State.Destroyed, Snapshot.State.Error))
127+
.thenReturn(List.of(existing));
128+
129+
Assert.assertThrows(InvalidParameterValueException.class, () ->
130+
snapshotManager.allocSnapshot(volumeId, Snapshot.MANUAL_POLICY_ID, "dup", null));
131+
} finally {
132+
CallContext.unregister();
133+
}
134+
}
135+
92136
@Test
93137
public void testGetSnapshotZoneImageStoreValid() {
94138
final long snapshotId = 1L;

0 commit comments

Comments
 (0)