diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java index 016ab0e993f..6ed8db66582 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java @@ -102,6 +102,7 @@ import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.util.LinstorConfigurationManager; import org.apache.cloudstack.storage.datastore.util.LinstorUtil; import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.storage.volume.VolumeObject; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; @@ -798,6 +799,15 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver || srcData.getDataStore().getRole() == DataStoreRole.ImageCache); } + private static boolean canCopyVolumeCond(DataObject srcData, DataObject dstData) { + // Volume download from Linstor primary storage + return srcData.getType() == DataObjectType.VOLUME + && (dstData.getType() == DataObjectType.VOLUME || dstData.getType() == DataObjectType.TEMPLATE) + && srcData.getDataStore().getRole() == DataStoreRole.Primary + && (dstData.getDataStore().getRole() == DataStoreRole.Image + || dstData.getDataStore().getRole() == DataStoreRole.ImageCache); + } + @Override public boolean canCopy(DataObject srcData, DataObject dstData) { @@ -814,6 +824,10 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver return storagePoolVO != null && storagePoolVO.getPoolType() == Storage.StoragePoolType.Linstor && tInfo.getSize() != null; + } else if (canCopyVolumeCond(srcData, dstData)) { + VolumeInfo srcVolInfo = (VolumeInfo) srcData; + StoragePoolVO storagePool = _storagePoolDao.findById(srcVolInfo.getPoolId()); + return storagePool.getStorageProviderName().equals(LinstorUtil.PROVIDER_NAME); } return false; } @@ -844,6 +858,9 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver } else if (canCopyTemplateCond(srcData, dstData)) { Answer answer = copyTemplate(srcData, dstData); res = new CopyCommandResult(null, answer); + } else if (canCopyVolumeCond(srcData, dstData)) { + Answer answer = copyVolume(srcData, dstData); + res = new CopyCommandResult(null, answer); } else { Answer answer = new Answer(null, false, "noimpl"); res = new CopyCommandResult(null, answer); @@ -965,6 +982,43 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver return answer; } + private Answer copyVolume(DataObject srcData, DataObject dstData) { + VolumeInfo srcVolInfo = (VolumeInfo) srcData; + final StoragePoolVO pool = _storagePoolDao.findById(srcVolInfo.getDataStore().getId()); + final DevelopersApi api = LinstorUtil.getLinstorAPI(pool.getHostAddress()); + final String rscName = LinstorUtil.RSC_PREFIX + srcVolInfo.getUuid(); + + VolumeObjectTO to = (VolumeObjectTO) srcVolInfo.getTO(); + // patch source format + // Linstor volumes are stored as RAW, but we can't set the correct format as RAW (we use QCOW2) + // otherwise create template from snapshot won't work, because this operation + // uses the format of the base volume and we backup snapshots as QCOW2 + // https://github.com/apache/cloudstack/pull/8802#issuecomment-2024019927 + to.setFormat(Storage.ImageFormat.RAW); + int nMaxExecutionSeconds = NumbersUtil.parseInt( + _configDao.getValue(Config.CopyVolumeWait.key()), 10800); + CopyCommand cmd = new CopyCommand( + to, + dstData.getTO(), + nMaxExecutionSeconds, + VirtualMachineManager.ExecuteInSequence.value()); + Answer answer; + + try { + Optional optEP = getLinstorEP(api, rscName); + if (optEP.isPresent()) { + answer = optEP.get().sendMessage(cmd); + } + else { + answer = new Answer(cmd, false, "Unable to get matching Linstor endpoint."); + } + } catch (ApiException exc) { + logger.error("copy volume failed: ", exc); + throw new CloudRuntimeException(exc.getBestMessage()); + } + return answer; + } + /** * Create a temporary resource from the snapshot to backup, so we can copy the data on a diskless agent * @param api Linstor Developer api object diff --git a/test/integration/smoke/test_restore_vm.py b/test/integration/smoke/test_restore_vm.py index ec0383d17a8..dd33346ed9e 100644 --- a/test/integration/smoke/test_restore_vm.py +++ b/test/integration/smoke/test_restore_vm.py @@ -32,6 +32,7 @@ class TestRestoreVM(cloudstackTestCase): testClient = super(TestRestoreVM, cls).getClsTestClient() cls.apiclient = testClient.getApiClient() cls.services = testClient.getParsedTestDataConfig() + cls._cleanup = [] # Get Zone, Domain and templates cls.domain = get_domain(cls.apiclient) @@ -42,16 +43,21 @@ class TestRestoreVM(cloudstackTestCase): cls.services["virtual_machine"]["zoneid"] = cls.zone.id cls.service_offering = ServiceOffering.create(cls.apiclient, cls.services["service_offering"]) + cls._cleanup.append(cls.service_offering) - cls.template_t1 = Template.register(cls.apiclient, cls.services["test_templates"][ + template_t1 = Template.register(cls.apiclient, cls.services["test_templates"][ cls.hypervisor.lower() if cls.hypervisor.lower() != 'simulator' else 'xenserver'], zoneid=cls.zone.id, hypervisor=cls.hypervisor.lower()) + cls._cleanup.append(template_t1) + template_t1.download(cls.apiclient) + cls.template_t1 = Template.list(cls.apiclient, templatefilter='all', id=template_t1.id)[0] - cls.template_t2 = Template.register(cls.apiclient, cls.services["test_templates"][ + template_t2 = Template.register(cls.apiclient, cls.services["test_templates"][ cls.hypervisor.lower() if cls.hypervisor.lower() != 'simulator' else 'xenserver'], zoneid=cls.zone.id, hypervisor=cls.hypervisor.lower()) - - cls._cleanup = [cls.service_offering, cls.template_t1, cls.template_t2] + cls._cleanup.append(template_t2) + template_t2.download(cls.apiclient) + cls.template_t2 = Template.list(cls.apiclient, templatefilter='all', id=template_t2.id)[0] @classmethod def tearDownClass(cls):