Skip to content

Commit 9b8eaee

Browse files
authored
Fix: Convert volume to another directory instead of copying it while taking volume snapshots on KVM (#8041)
1 parent 51add0a commit 9b8eaee

File tree

2 files changed

+66
-28
lines changed

2 files changed

+66
-28
lines changed

plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,11 +1715,11 @@ public Answer createSnapshot(final CreateObjectCommand cmd) {
17151715
snapshotPath = getSnapshotPathInPrimaryStorage(primaryPool.getLocalPath(), snapshotName);
17161716

17171717
String diskLabel = takeVolumeSnapshot(resource.getDisks(conn, vmName), snapshotName, diskPath, vm);
1718-
String copyResult = copySnapshotToPrimaryStorageDir(primaryPool, diskPath, snapshotPath, volume);
1718+
String convertResult = convertBaseFileToSnapshotFileInPrimaryStorageDir(primaryPool, diskPath, snapshotPath, volume, cmd.getWait());
17191719

17201720
mergeSnapshotIntoBaseFile(vm, diskLabel, diskPath, snapshotName, volume, conn);
17211721

1722-
validateCopyResult(copyResult, snapshotPath);
1722+
validateConvertResult(convertResult, snapshotPath);
17231723
} catch (LibvirtException e) {
17241724
if (!e.getMessage().contains(LIBVIRT_OPERATION_NOT_SUPPORTED_MESSAGE)) {
17251725
throw e;
@@ -1784,8 +1784,8 @@ public Answer createSnapshot(final CreateObjectCommand cmd) {
17841784
}
17851785
} else {
17861786
snapshotPath = getSnapshotPathInPrimaryStorage(primaryPool.getLocalPath(), snapshotName);
1787-
String copyResult = copySnapshotToPrimaryStorageDir(primaryPool, diskPath, snapshotPath, volume);
1788-
validateCopyResult(copyResult, snapshotPath);
1787+
String convertResult = convertBaseFileToSnapshotFileInPrimaryStorageDir(primaryPool, diskPath, snapshotPath, volume, cmd.getWait());
1788+
validateConvertResult(convertResult, snapshotPath);
17891789
}
17901790
}
17911791

@@ -1838,13 +1838,13 @@ protected void takeFullVmSnapshotForBinariesThatDoesNotSupportLiveDiskSnapshot(D
18381838
s_logger.debug(String.format("Full VM Snapshot [%s] of VM [%s] took [%s] seconds to finish.", snapshotName, vmName, (System.currentTimeMillis() - start)/1000));
18391839
}
18401840

1841-
protected void validateCopyResult(String copyResult, String snapshotPath) throws CloudRuntimeException, IOException {
1842-
if (copyResult == null) {
1841+
protected void validateConvertResult(String convertResult, String snapshotPath) throws CloudRuntimeException, IOException {
1842+
if (convertResult == null) {
18431843
return;
18441844
}
18451845

18461846
Files.deleteIfExists(Paths.get(snapshotPath));
1847-
throw new CloudRuntimeException(copyResult);
1847+
throw new CloudRuntimeException(convertResult);
18481848
}
18491849

18501850
/**
@@ -1901,20 +1901,31 @@ protected void manuallyDeleteUnusedSnapshotFile(boolean isLibvirtSupportingFlagD
19011901
}
19021902

19031903
/**
1904-
* Creates the snapshot directory in the primary storage, if it does not exist; then copies the base file (VM's old writing file) to the snapshot dir..
1904+
* Creates the snapshot directory in the primary storage, if it does not exist; then, converts the base file (VM's old writing file) to the snapshot directory.
19051905
* @param primaryPool Storage to create folder, if not exists;
1906-
* @param baseFile Base file of VM, which will be copied;
1907-
* @param snapshotPath Path to copy the base file;
1908-
* @return null if copies successfully or a error message.
1906+
* @param baseFile Base file of VM, which will be converted;
1907+
* @param snapshotPath Path to convert the base file;
1908+
* @return null if the conversion occurs successfully or an error message that must be handled.
19091909
*/
1910-
protected String copySnapshotToPrimaryStorageDir(KVMStoragePool primaryPool, String baseFile, String snapshotPath, VolumeObjectTO volume) {
1910+
protected String convertBaseFileToSnapshotFileInPrimaryStorageDir(KVMStoragePool primaryPool, String baseFile, String snapshotPath, VolumeObjectTO volume, int wait) {
19111911
try {
1912+
s_logger.debug(String.format("Trying to convert volume [%s] (%s) to snapshot [%s].", volume, baseFile, snapshotPath));
1913+
19121914
primaryPool.createFolder(TemplateConstants.DEFAULT_SNAPSHOT_ROOT_DIR);
1913-
Files.copy(Paths.get(baseFile), Paths.get(snapshotPath));
1914-
s_logger.debug(String.format("Copied %s snapshot from [%s] to [%s].", volume, baseFile, snapshotPath));
1915+
1916+
QemuImgFile srcFile = new QemuImgFile(baseFile);
1917+
srcFile.setFormat(PhysicalDiskFormat.QCOW2);
1918+
1919+
QemuImgFile destFile = new QemuImgFile(snapshotPath);
1920+
destFile.setFormat(PhysicalDiskFormat.QCOW2);
1921+
1922+
QemuImg q = new QemuImg(wait);
1923+
q.convert(srcFile, destFile);
1924+
1925+
s_logger.debug(String.format("Converted volume [%s] (from path \"%s\") to snapshot [%s].", volume, baseFile, snapshotPath));
19151926
return null;
1916-
} catch (IOException ex) {
1917-
return String.format("Unable to copy %s snapshot [%s] to [%s] due to [%s].", volume, baseFile, snapshotPath, ex.getMessage());
1927+
} catch (QemuImgException | LibvirtException ex) {
1928+
return String.format("Failed to convert %s snapshot of volume [%s] to [%s] due to [%s].", volume, baseFile, snapshotPath, ex.getMessage());
19181929
}
19191930
}
19201931

plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
import java.util.Set;
4040
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
4141
import org.apache.cloudstack.storage.to.VolumeObjectTO;
42+
import org.apache.cloudstack.utils.qemu.QemuImg;
43+
import org.apache.cloudstack.utils.qemu.QemuImgException;
44+
import org.apache.cloudstack.utils.qemu.QemuImgFile;
4245
import org.junit.Assert;
4346
import org.junit.Before;
4447
import org.junit.Test;
@@ -91,6 +94,9 @@ public class KVMStorageProcessorTest {
9194
@Mock
9295
Connect connectMock;
9396

97+
@Mock
98+
QemuImg qemuImgMock;
99+
94100
@Mock
95101
LibvirtDomainXMLParser libvirtDomainXMLParserMock;
96102
@Mock
@@ -251,32 +257,53 @@ public void validateTakeVolumeSnapshotSuccessReturnDiskLabel() throws LibvirtExc
251257

252258
@Test
253259
@PrepareForTest(KVMStorageProcessor.class)
254-
public void validateCopySnapshotToPrimaryStorageDirFailToCopyReturnErrorMessage() throws Exception {
260+
public void convertBaseFileToSnapshotFileInPrimaryStorageDirTestFailToConvertWithQemuImgExceptionReturnErrorMessage() throws Exception {
255261
String baseFile = "baseFile";
256262
String snapshotPath = "snapshotPath";
257263
String errorMessage = "error";
258-
String expectedResult = String.format("Unable to copy %s snapshot [%s] to [%s] due to [%s].", volumeObjectToMock, baseFile, snapshotPath, errorMessage);
264+
String expectedResult = String.format("Failed to convert %s snapshot of volume [%s] to [%s] due to [%s].", volumeObjectToMock, baseFile, snapshotPath, errorMessage);
259265

260266
Mockito.doReturn(true).when(kvmStoragePoolMock).createFolder(Mockito.anyString());
261-
PowerMockito.mockStatic(Files.class);
262-
PowerMockito.when(Files.copy(Mockito.any(Path.class), Mockito.any(Path.class), Mockito.any())).thenThrow(new IOException(errorMessage));
263267

264-
String result = storageProcessorSpy.copySnapshotToPrimaryStorageDir(kvmStoragePoolMock, baseFile, snapshotPath, volumeObjectToMock);
268+
PowerMockito.whenNew(QemuImg.class).withArguments(Mockito.anyInt()).thenReturn(qemuImgMock);
269+
Mockito.doThrow(new QemuImgException(errorMessage)).when(qemuImgMock).convert(Mockito.any(QemuImgFile.class), Mockito.any(QemuImgFile.class));
270+
271+
String result = storageProcessorSpy.convertBaseFileToSnapshotFileInPrimaryStorageDir(kvmStoragePoolMock, baseFile, snapshotPath, volumeObjectToMock, 1);
265272

266273
Assert.assertEquals(expectedResult, result);
267274
}
268275

269276
@Test
270277
@PrepareForTest(KVMStorageProcessor.class)
271-
public void validateCopySnapshotToPrimaryStorageDirCopySuccessReturnNull() throws Exception {
278+
public void convertBaseFileToSnapshotFileInPrimaryStorageDirTestFailToConvertWithLibvirtExceptionReturnErrorMessage() throws Exception {
272279
String baseFile = "baseFile";
273280
String snapshotPath = "snapshotPath";
281+
String errorMessage = "null";
282+
String expectedResult = String.format("Failed to convert %s snapshot of volume [%s] to [%s] due to [%s].", volumeObjectToMock, baseFile, snapshotPath, errorMessage);
274283

275284
Mockito.doReturn(true).when(kvmStoragePoolMock).createFolder(Mockito.anyString());
276-
PowerMockito.mockStatic(Files.class);
277-
PowerMockito.when(Files.copy(Mockito.any(Path.class), Mockito.any(Path.class), Mockito.any())).thenReturn(null);
278285

279-
String result = storageProcessorSpy.copySnapshotToPrimaryStorageDir(kvmStoragePoolMock, baseFile, snapshotPath, volumeObjectToMock);
286+
PowerMockito.whenNew(QemuImg.class).withArguments(Mockito.anyInt()).thenReturn(qemuImgMock);
287+
Mockito.doThrow(LibvirtException.class).when(qemuImgMock).convert(Mockito.any(QemuImgFile.class), Mockito.any(QemuImgFile.class));
288+
289+
String result = storageProcessorSpy.convertBaseFileToSnapshotFileInPrimaryStorageDir(kvmStoragePoolMock, baseFile, snapshotPath, volumeObjectToMock, 1);
290+
291+
Assert.assertEquals(expectedResult, result);
292+
}
293+
294+
295+
@Test
296+
@PrepareForTest(KVMStorageProcessor.class)
297+
public void convertBaseFileToSnapshotFileInPrimaryStorageDirTestConvertSuccessReturnNull() throws Exception {
298+
String baseFile = "baseFile";
299+
String snapshotPath = "snapshotPath";
300+
301+
Mockito.doReturn(true).when(kvmStoragePoolMock).createFolder(Mockito.anyString());
302+
303+
PowerMockito.whenNew(QemuImg.class).withArguments(Mockito.anyInt()).thenReturn(qemuImgMock);
304+
Mockito.doNothing().when(qemuImgMock).convert(Mockito.any(QemuImgFile.class), Mockito.any(QemuImgFile.class));
305+
306+
String result = storageProcessorSpy.convertBaseFileToSnapshotFileInPrimaryStorageDir(kvmStoragePoolMock, baseFile, snapshotPath, volumeObjectToMock, 1);
280307

281308
Assert.assertNull(result);
282309
}
@@ -321,22 +348,22 @@ public void validateIsAvailablePoolSizeDividedByDiskSizeLesserThanMinRate(){
321348

322349
@Test
323350
public void validateValidateCopyResultResultIsNullReturn() throws CloudRuntimeException, IOException{
324-
storageProcessorSpy.validateCopyResult(null, "");
351+
storageProcessorSpy.validateConvertResult(null, "");
325352
}
326353

327354
@Test (expected = IOException.class)
328355
public void validateValidateCopyResultFailToDeleteThrowIOException() throws CloudRuntimeException, IOException{
329356
PowerMockito.mockStatic(Files.class);
330357
PowerMockito.when(Files.deleteIfExists(Mockito.any())).thenThrow(new IOException(""));
331-
storageProcessorSpy.validateCopyResult("", "");
358+
storageProcessorSpy.validateConvertResult("", "");
332359
}
333360

334361
@Test (expected = CloudRuntimeException.class)
335362
@PrepareForTest(KVMStorageProcessor.class)
336363
public void validateValidateCopyResulResultNotNullThrowCloudRuntimeException() throws CloudRuntimeException, IOException{
337364
PowerMockito.mockStatic(Files.class);
338365
PowerMockito.when(Files.deleteIfExists(Mockito.any())).thenReturn(true);
339-
storageProcessorSpy.validateCopyResult("", "");
366+
storageProcessorSpy.validateConvertResult("", "");
340367
}
341368

342369
@Test (expected = CloudRuntimeException.class)

0 commit comments

Comments
 (0)