Skip to content

Commit 7936eb0

Browse files
GutoVeroneziGutoVeronezi
andauthored
server: Fix delete parent snapshot (#6630)
ACS + Xenserver works with differential snapshots. ACS takes a volume full snapshot and the next ones are referenced as a child of the previous snapshot until the chain reaches the limit defined in the global setting snapshot.delta.max; then, a new full snapshot is taken. PR #5297 introduced disk-only snapshots for KVM volumes. Among the changes, the delete process was also refactored. Before the changes, when one was removing a snapshot with children, ACS was marking it as Destroyed and it was keeping the Image entry on the table cloud.snapshot_store_ref as Ready. When ACS was rotating the snapshots (the max delta was reached) and all the children were already marked as removed; then, ACS would start removing the whole hierarchy, completing the differential snapshot cycle. After the changes, the snapshots with children stopped being marked as removed and the differential snapshot cycle was not being completed. This PR intends to honor again the differential snapshot cycle for XenServer, making the snapshots to be marked as removed when deleted while having children and following the differential snapshot cycle. Also, when one takes a volume snapshot and ACS backs it up to the secondary storage, ACS inserts 2 entries on table cloud.snapshot_store_ref (Primary and Image). When one deletes a volume snapshot, ACS first tries to remove the snapshot from the secondary storage and mark the entry Image as removed; then, it tries to remove the snapshot from the primary storage and mark the entry Primary as removed. If ACS cannot remove the snapshot from the primary storage, it will keep the snapshot as BackedUp; however, If it does not exist in the secondary storage and without the entry SNAPSHOT.DELETE on cloud.usage_event. In the end, after the garbage collector flow, the snapshot will be marked as BackedUp, with a value in the field removed and still being rated. This PR also addresses the correction for this situation. Co-authored-by: GutoVeronezi <[email protected]>
1 parent 5a54dc1 commit 7936eb0

File tree

2 files changed

+148
-20
lines changed

2 files changed

+148
-20
lines changed

engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323

2424
import javax.inject.Inject;
2525

26+
import com.cloud.storage.VolumeDetailVO;
27+
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
28+
import org.apache.commons.collections.CollectionUtils;
29+
import org.apache.commons.lang3.BooleanUtils;
2630
import org.apache.log4j.Logger;
2731
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
2832
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
@@ -302,47 +306,85 @@ protected void updateSnapshotToDestroyed(SnapshotVO snapshotVo) {
302306
protected boolean deleteSnapshotInfos(SnapshotVO snapshotVo) {
303307
Map<String, SnapshotInfo> snapshotInfos = retrieveSnapshotEntries(snapshotVo.getId());
304308

309+
boolean result = false;
305310
for (var infoEntry : snapshotInfos.entrySet()) {
306-
if (!deleteSnapshotInfo(infoEntry.getValue(), infoEntry.getKey(), snapshotVo)) {
307-
return false;
311+
if (BooleanUtils.toBooleanDefaultIfNull(deleteSnapshotInfo(infoEntry.getValue(), infoEntry.getKey(), snapshotVo), false)) {
312+
result = true;
308313
}
309314
}
310315

311-
return true;
316+
return result;
312317
}
313318

314319
/**
315320
* Destroys the snapshot entry and file.
316321
* @return true if destroy successfully, else false.
317322
*/
318-
protected boolean deleteSnapshotInfo(SnapshotInfo snapshotInfo, String storage, SnapshotVO snapshotVo) {
323+
protected Boolean deleteSnapshotInfo(SnapshotInfo snapshotInfo, String storage, SnapshotVO snapshotVo) {
319324
if (snapshotInfo == null) {
320-
s_logger.debug(String.format("Could not find %s entry on a %s. Skipping deletion on %s.", snapshotVo, storage, storage));
321-
return true;
325+
s_logger.debug(String.format("Could not find %s entry on %s. Skipping deletion on %s.", snapshotVo, storage, storage));
326+
return SECONDARY_STORAGE_SNAPSHOT_ENTRY_IDENTIFIER.equals(storage) ? null : true;
322327
}
323328

324329
DataStore dataStore = snapshotInfo.getDataStore();
325-
storage = String.format("%s {uuid: \"%s\", name: \"%s\"}", storage, dataStore.getUuid(), dataStore.getName());
330+
String storageToString = String.format("%s {uuid: \"%s\", name: \"%s\"}", storage, dataStore.getUuid(), dataStore.getName());
326331

327332
try {
328333
SnapshotObject snapshotObject = castSnapshotInfoToSnapshotObject(snapshotInfo);
329334
snapshotObject.processEvent(Snapshot.Event.DestroyRequested);
330335

331-
if (deleteSnapshotChain(snapshotInfo, storage)) {
336+
if (SECONDARY_STORAGE_SNAPSHOT_ENTRY_IDENTIFIER.equals(storage)) {
337+
338+
verifyIfTheSnapshotIsBeingUsedByAnyVolume(snapshotObject);
339+
340+
if (deleteSnapshotChain(snapshotInfo, storageToString)) {
341+
s_logger.debug(String.format("%s was deleted on %s. We will mark the snapshot as destroyed.", snapshotVo, storageToString));
342+
} else {
343+
s_logger.debug(String.format("%s was not deleted on %s; however, we will mark the snapshot as destroyed for future garbage collecting.", snapshotVo,
344+
storageToString));
345+
}
346+
332347
snapshotObject.processEvent(Snapshot.Event.OperationSucceeded);
333-
s_logger.debug(String.format("%s was deleted on %s.", snapshotVo, storage));
348+
return true;
349+
} else if (deleteSnapshotInPrimaryStorage(snapshotInfo, snapshotVo, storageToString, snapshotObject)) {
334350
return true;
335351
}
336352

353+
s_logger.debug(String.format("Failed to delete %s on %s.", snapshotVo, storageToString));
337354
snapshotObject.processEvent(Snapshot.Event.OperationFailed);
338-
s_logger.debug(String.format("Failed to delete %s on %s.", snapshotVo, storage));
339355
} catch (NoTransitionException ex) {
340-
s_logger.warn(String.format("Failed to delete %s on %s due to %s.", snapshotVo, storage, ex.getMessage()), ex);
356+
s_logger.warn(String.format("Failed to delete %s on %s due to %s.", snapshotVo, storageToString, ex.getMessage()), ex);
341357
}
342358

343359
return false;
344360
}
345361

362+
protected boolean deleteSnapshotInPrimaryStorage(SnapshotInfo snapshotInfo, SnapshotVO snapshotVo, String storageToString, SnapshotObject snapshotObject) throws NoTransitionException {
363+
try {
364+
if (snapshotSvr.deleteSnapshot(snapshotInfo)) {
365+
snapshotObject.processEvent(Snapshot.Event.OperationSucceeded);
366+
s_logger.debug(String.format("%s was deleted on %s. We will mark the snapshot as destroyed.", snapshotVo, storageToString));
367+
return true;
368+
}
369+
} catch (CloudRuntimeException ex) {
370+
s_logger.warn(String.format("Unable do delete snapshot %s on %s due to [%s]. The reference will be marked as 'Destroying' for future garbage collecting.",
371+
snapshotVo, storageToString, ex.getMessage()), ex);
372+
}
373+
return false;
374+
}
375+
376+
protected void verifyIfTheSnapshotIsBeingUsedByAnyVolume(SnapshotObject snapshotObject) throws NoTransitionException {
377+
List<VolumeDetailVO> volumesFromSnapshot = _volumeDetailsDaoImpl.findDetails("SNAPSHOT_ID", String.valueOf(snapshotObject.getSnapshotId()), null);
378+
if (CollectionUtils.isEmpty(volumesFromSnapshot)) {
379+
return;
380+
}
381+
382+
snapshotObject.processEvent(Snapshot.Event.OperationFailed);
383+
throw new CloudRuntimeException(String.format("Unable to delete snapshot [%s] because it is being used by the following volumes: %s.",
384+
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(snapshotObject.getSnapshotVO(), "id", "uuid", "volumeId", "name"),
385+
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(volumesFromSnapshot, "resourceId")));
386+
}
387+
346388
/**
347389
* Cast SnapshotInfo to SnapshotObject.
348390
* @return SnapshotInfo cast to SnapshotObject.

engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategyTest.java

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,23 @@
1717

1818
package org.apache.cloudstack.storage.snapshot;
1919

20+
import java.util.ArrayList;
2021
import java.util.LinkedHashMap;
22+
import java.util.List;
2123
import java.util.Map;
2224

25+
import com.cloud.storage.VolumeDetailVO;
26+
import com.cloud.storage.dao.VolumeDetailsDao;
27+
import com.cloud.utils.exception.CloudRuntimeException;
2328
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
2429
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory;
2530
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
31+
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService;
2632
import org.junit.Assert;
2733
import org.junit.Before;
2834
import org.junit.Test;
2935
import org.junit.runner.RunWith;
36+
import org.mockito.InjectMocks;
3037
import org.mockito.Mock;
3138
import org.mockito.Mockito;
3239
import org.mockito.junit.MockitoJUnitRunner;
@@ -40,6 +47,7 @@
4047
@RunWith(MockitoJUnitRunner.class)
4148
public class DefaultSnapshotStrategyTest {
4249

50+
@InjectMocks
4351
DefaultSnapshotStrategy defaultSnapshotStrategySpy = Mockito.spy(DefaultSnapshotStrategy.class);
4452

4553
@Mock
@@ -60,15 +68,18 @@ public class DefaultSnapshotStrategyTest {
6068
@Mock
6169
DataStore dataStoreMock;
6270

71+
@Mock
72+
VolumeDetailsDao volumeDetailsDaoMock;
73+
74+
@Mock
75+
SnapshotService snapshotServiceMock;
76+
6377
Map<String, SnapshotInfo> mapStringSnapshotInfoInstance = new LinkedHashMap<>();
6478

6579
@Before
6680
public void setup() {
67-
defaultSnapshotStrategySpy.snapshotDataFactory = snapshotDataFactoryMock;
68-
defaultSnapshotStrategySpy.snapshotDao = snapshotDaoMock;
69-
7081
mapStringSnapshotInfoInstance.put("secondary storage", snapshotInfo1Mock);
71-
mapStringSnapshotInfoInstance.put("priamry storage", snapshotInfo1Mock);
82+
mapStringSnapshotInfoInstance.put("primary storage", snapshotInfo1Mock);
7283
}
7384

7485
@Test
@@ -122,7 +133,7 @@ public void validateDeleteSnapshotInfosDeletesSuccessfullyReturnsTrue() {
122133

123134
@Test
124135
public void validateDeleteSnapshotInfoSnapshotInfoIsNullOnSecondaryStorageReturnsTrue() {
125-
Assert.assertTrue(defaultSnapshotStrategySpy.deleteSnapshotInfo(null, "secondary storage", snapshotVoMock));
136+
Assert.assertNull(defaultSnapshotStrategySpy.deleteSnapshotInfo(null, "secondary storage", snapshotVoMock));
126137
}
127138

128139
@Test
@@ -131,24 +142,60 @@ public void validateDeleteSnapshotInfoSnapshotInfoIsNullOnPrimaryStorageReturnsF
131142
}
132143

133144
@Test
134-
public void validateDeleteSnapshotInfoSnapshotDeleteSnapshotChainSuccessfullyReturnsTrue() throws NoTransitionException {
145+
public void deleteSnapshotInfoTestReturnTrueIfCanDeleteTheSnapshotOnPrimaryStorage() throws NoTransitionException {
146+
Mockito.doReturn(dataStoreMock).when(snapshotInfo1Mock).getDataStore();
147+
Mockito.doReturn(snapshotObjectMock).when(defaultSnapshotStrategySpy).castSnapshotInfoToSnapshotObject(snapshotInfo1Mock);
148+
Mockito.doNothing().when(snapshotObjectMock).processEvent(Mockito.any(Snapshot.Event.class));
149+
Mockito.doReturn(true).when(snapshotServiceMock).deleteSnapshot(Mockito.any());
150+
151+
boolean result = defaultSnapshotStrategySpy.deleteSnapshotInfo(snapshotInfo1Mock, "primary storage", snapshotVoMock);
152+
Assert.assertTrue(result);
153+
}
154+
155+
@Test
156+
public void deleteSnapshotInfoTestReturnFalseIfCannotDeleteTheSnapshotOnPrimaryStorage() throws NoTransitionException {
157+
Mockito.doReturn(dataStoreMock).when(snapshotInfo1Mock).getDataStore();
158+
Mockito.doReturn(snapshotObjectMock).when(defaultSnapshotStrategySpy).castSnapshotInfoToSnapshotObject(snapshotInfo1Mock);
159+
Mockito.doNothing().when(snapshotObjectMock).processEvent(Mockito.any(Snapshot.Event.class));
160+
Mockito.doReturn(false).when(snapshotServiceMock).deleteSnapshot(Mockito.any());
161+
162+
boolean result = defaultSnapshotStrategySpy.deleteSnapshotInfo(snapshotInfo1Mock, "primary storage", snapshotVoMock);
163+
Assert.assertFalse(result);
164+
}
165+
166+
@Test
167+
public void deleteSnapshotInfoTestReturnFalseIfDeleteSnapshotOnPrimaryStorageThrowsACloudRuntimeException() throws NoTransitionException {
168+
Mockito.doReturn(dataStoreMock).when(snapshotInfo1Mock).getDataStore();
169+
Mockito.doReturn(snapshotObjectMock).when(defaultSnapshotStrategySpy).castSnapshotInfoToSnapshotObject(snapshotInfo1Mock);
170+
Mockito.doNothing().when(snapshotObjectMock).processEvent(Mockito.any(Snapshot.Event.class));
171+
Mockito.doThrow(CloudRuntimeException.class).when(snapshotServiceMock).deleteSnapshot(Mockito.any());
172+
173+
boolean result = defaultSnapshotStrategySpy.deleteSnapshotInfo(snapshotInfo1Mock, "primary storage", snapshotVoMock);
174+
Assert.assertFalse(result);
175+
}
176+
177+
@Test
178+
public void deleteSnapshotInfoTestReturnTrueIfCanDeleteTheSnapshotChainForSecondaryStorage() throws NoTransitionException {
135179
Mockito.doReturn(dataStoreMock).when(snapshotInfo1Mock).getDataStore();
136180
Mockito.doReturn(snapshotObjectMock).when(defaultSnapshotStrategySpy).castSnapshotInfoToSnapshotObject(snapshotInfo1Mock);
181+
Mockito.doNothing().when(defaultSnapshotStrategySpy).verifyIfTheSnapshotIsBeingUsedByAnyVolume(snapshotObjectMock);
137182
Mockito.doNothing().when(snapshotObjectMock).processEvent(Mockito.any(Snapshot.Event.class));
138183
Mockito.doReturn(true).when(defaultSnapshotStrategySpy).deleteSnapshotChain(Mockito.any(), Mockito.anyString());
139184

140-
Assert.assertTrue(defaultSnapshotStrategySpy.deleteSnapshotInfo(snapshotInfo1Mock, "secondary storage", snapshotVoMock));
185+
boolean result = defaultSnapshotStrategySpy.deleteSnapshotInfo(snapshotInfo1Mock, "secondary storage", snapshotVoMock);
186+
Assert.assertTrue(result);
141187
}
142188

143189
@Test
144-
public void validateDeleteSnapshotInfoSnapshotDeleteSnapshotChainFails() throws NoTransitionException {
190+
public void deleteSnapshotInfoTestReturnTrueIfCannotDeleteTheSnapshotChainForSecondaryStorage() throws NoTransitionException {
145191
Mockito.doReturn(dataStoreMock).when(snapshotInfo1Mock).getDataStore();
146192
Mockito.doReturn(snapshotObjectMock).when(defaultSnapshotStrategySpy).castSnapshotInfoToSnapshotObject(snapshotInfo1Mock);
193+
Mockito.doNothing().when(defaultSnapshotStrategySpy).verifyIfTheSnapshotIsBeingUsedByAnyVolume(snapshotObjectMock);
147194
Mockito.doNothing().when(snapshotObjectMock).processEvent(Mockito.any(Snapshot.Event.class));
148195
Mockito.doReturn(false).when(defaultSnapshotStrategySpy).deleteSnapshotChain(Mockito.any(), Mockito.anyString());
149196

150197
boolean result = defaultSnapshotStrategySpy.deleteSnapshotInfo(snapshotInfo1Mock, "secondary storage", snapshotVoMock);
151-
Assert.assertFalse(result);
198+
Assert.assertTrue(result);
152199
}
153200

154201
@Test
@@ -159,4 +206,43 @@ public void validateDeleteSnapshotInfoSnapshotProcessSnapshotEventThrowsNoTransi
159206

160207
Assert.assertFalse(defaultSnapshotStrategySpy.deleteSnapshotInfo(snapshotInfo1Mock, "secondary storage", snapshotVoMock));
161208
}
209+
210+
@Test
211+
public void verifyIfTheSnapshotIsBeingUsedByAnyVolumeTestDetailsIsEmptyDoNothing() throws NoTransitionException {
212+
Mockito.doReturn(new ArrayList<>()).when(volumeDetailsDaoMock).findDetails(Mockito.any(), Mockito.any(), Mockito.any());
213+
defaultSnapshotStrategySpy.verifyIfTheSnapshotIsBeingUsedByAnyVolume(snapshotObjectMock);
214+
Mockito.verify(snapshotObjectMock, Mockito.never()).processEvent(Mockito.any(Snapshot.Event.class));
215+
}
216+
217+
@Test
218+
public void verifyIfTheSnapshotIsBeingUsedByAnyVolumeTestDetailsIsNullDoNothing() throws NoTransitionException {
219+
Mockito.doReturn(null).when(volumeDetailsDaoMock).findDetails(Mockito.any(), Mockito.any(), Mockito.any());
220+
defaultSnapshotStrategySpy.verifyIfTheSnapshotIsBeingUsedByAnyVolume(snapshotObjectMock);
221+
Mockito.verify(snapshotObjectMock, Mockito.never()).processEvent(Mockito.any(Snapshot.Event.class));
222+
}
223+
224+
@Test(expected = CloudRuntimeException.class)
225+
public void verifyIfTheSnapshotIsBeingUsedByAnyVolumeTestDetailsIsNotEmptyThrowCloudRuntimeException() throws NoTransitionException {
226+
Mockito.doReturn(List.of(new VolumeDetailVO())).when(volumeDetailsDaoMock).findDetails(Mockito.any(), Mockito.any(), Mockito.any());
227+
defaultSnapshotStrategySpy.verifyIfTheSnapshotIsBeingUsedByAnyVolume(snapshotObjectMock);
228+
}
229+
230+
@Test
231+
public void deleteSnapshotInPrimaryStorageTestReturnTrueIfDeleteReturnsTrue() throws NoTransitionException {
232+
Mockito.doReturn(true).when(snapshotServiceMock).deleteSnapshot(Mockito.any());
233+
Mockito.doNothing().when(snapshotObjectMock).processEvent(Mockito.any(Snapshot.Event.class));
234+
Assert.assertTrue(defaultSnapshotStrategySpy.deleteSnapshotInPrimaryStorage(null, null, null, snapshotObjectMock));
235+
}
236+
237+
@Test
238+
public void deleteSnapshotInPrimaryStorageTestReturnFalseIfDeleteReturnsFalse() throws NoTransitionException {
239+
Mockito.doReturn(false).when(snapshotServiceMock).deleteSnapshot(Mockito.any());
240+
Assert.assertFalse(defaultSnapshotStrategySpy.deleteSnapshotInPrimaryStorage(null, null, null, null));
241+
}
242+
243+
@Test
244+
public void deleteSnapshotInPrimaryStorageTestReturnFalseIfDeleteThrowsException() throws NoTransitionException {
245+
Mockito.doThrow(CloudRuntimeException.class).when(snapshotServiceMock).deleteSnapshot(Mockito.any());
246+
Assert.assertFalse(defaultSnapshotStrategySpy.deleteSnapshotInPrimaryStorage(null, null, null, null));
247+
}
162248
}

0 commit comments

Comments
 (0)