diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java index a32b8dd604e7..14c2159219d5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java @@ -92,7 +92,7 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd { @Parameter(name = ApiConstants.DISK_PATH, type = CommandType.STRING, - description = "path of the disk image. It is the file name on file based storage pools (NFS, Local, SharedMountPoint), and the image name on RBD storage pools") + description = "path of the disk image. It is the file name on file based storage pools (NFS, Local, SharedMountPoint), the image name on RBD storage pools, and the resource name without the cs- prefix on Linstor storage pools") private String diskPath; @Parameter(name = ApiConstants.IMPORT_SOURCE, diff --git a/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java b/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java index 13f01b4944ad..709ad691971a 100644 --- a/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java +++ b/api/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageService.java @@ -37,7 +37,8 @@ public interface VolumeImportUnmanageService extends PluggableService, Configura Arrays.asList(Hypervisor.HypervisorType.KVM, Hypervisor.HypervisorType.VMware); List SUPPORTED_STORAGE_POOL_TYPES_FOR_KVM = Arrays.asList(Storage.StoragePoolType.NetworkFilesystem, - Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.RBD, Storage.StoragePoolType.SharedMountPoint); + Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.RBD, Storage.StoragePoolType.SharedMountPoint, + Storage.StoragePoolType.Linstor); ConfigKey AllowImportVolumeWithBackingFile = new ConfigKey<>(Boolean.class, "allow.import.volume.with.backing.file", diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java index 3a42f230f845..f69dc1d05c29 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java @@ -51,7 +51,8 @@ public final class LibvirtCheckVolumeCommandWrapper extends CommandWrapper volumeDetails = new HashMap<>(); + volumeDetails.put(VolumeOnStorageTO.Detail.IS_LOCKED, String.valueOf(Boolean.TRUE)); + return new CheckVolumeAnswer(command, false, "", 0, volumeDetails); + } + pool.checkPhysicalDiskForImport(srcFile); + connected = pool.connectPhysicalDisk(srcFile, null); final KVMPhysicalDisk vol = pool.getPhysicalDisk(srcFile); final String path = vol.getPath(); - final boolean isRbd = Storage.StoragePoolType.RBD.equals(storageFilerTO.getType()); + final boolean isBlockDevice = Storage.StoragePoolType.RBD.equals(storageFilerTO.getType()) || + Storage.StoragePoolType.Linstor.equals(storageFilerTO.getType()); Map volumeDetails = getVolumeDetails(pool, vol); if (MapUtils.isEmpty(volumeDetails)) { return new Answer(command, false, "Unable to read the volume on the storage pool"); } - if (!isRbd) { + if (!isBlockDevice) { try { KVMPhysicalDisk.checkQcow2File(path); } catch (final CloudRuntimeException e) { @@ -80,9 +90,8 @@ public Answer execute(final CheckVolumeCommand command, final LibvirtComputingRe } } - // Images on RBD are raw and the path is an image name that qemu-img cannot open - // without the rbd: URI, so take the size libvirt already reported for the volume. - long size = isRbd ? vol.getVirtualSize() : KVMPhysicalDisk.getVirtualSizeFromFile(path); + // RBD and Linstor volumes are raw, so take the size the pool reported for the volume + long size = isBlockDevice ? vol.getVirtualSize() : KVMPhysicalDisk.getVirtualSizeFromFile(path); return new CheckVolumeAnswer(command, true, "", size, volumeDetails); } else { return new Answer(command, false, "Unsupported Storage Pool"); @@ -90,6 +99,10 @@ public Answer execute(final CheckVolumeCommand command, final LibvirtComputingRe } catch (final Exception e) { logger.error("Error while checking the disk: {}", e.getMessage()); return new Answer(command, false, result); + } finally { + if (connected) { + pool.disconnectPhysicalDisk(srcFile); + } } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java index ab6bdd6135d5..d62265dc6f26 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapper.java @@ -31,6 +31,7 @@ import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; @@ -74,6 +75,11 @@ public Answer execute(final GetVolumesOnStorageCommand command, final LibvirtCom private GetVolumesOnStorageAnswer addVolumeByVolumePath(final GetVolumesOnStorageCommand command, final KVMStoragePool storagePool, String volumePath) { List volumes = new ArrayList<>(); + try { + storagePool.checkPhysicalDiskForImport(volumePath); + } catch (CloudRuntimeException e) { + return new GetVolumesOnStorageAnswer(command, false, e.getMessage()); + } KVMPhysicalDisk disk = storagePool.getPhysicalDisk(volumePath); if (disk != null) { if (!volumePath.equals(disk.getPath()) && !volumePath.equals(disk.getName())) { @@ -97,6 +103,10 @@ private GetVolumesOnStorageAnswer addVolumeByVolumePath(final GetVolumesOnStorag return new GetVolumesOnStorageAnswer(command, false, String.format("The file format is %s, but expected to be %s", fileFormat, disk.getFormat())); } addDetailsToVolumeOnStorageTO(volumeOnStorageTO, info, storagePool, disk); + // qemu's lock only reveals a guest on this host for pools such as Linstor, so ask the pool + if (Boolean.TRUE.equals(storagePool.isPhysicalDiskInUse(volumePath))) { + volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.IS_LOCKED, String.valueOf(Boolean.TRUE)); + } volumes.add(volumeOnStorageTO); } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java index 3e35ed9476b1..dd880acd2e8c 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStoragePool.java @@ -51,6 +51,21 @@ default KVMPhysicalDisk createPhysicalDisk(String volumeUuid, PhysicalDiskFormat boolean disconnectPhysicalDisk(String volumeUuid); + /** + * Checks that a disk which already exists on the pool, and is about to be imported, belongs to this pool. + * Throws when it does not. Pools whose disks cannot belong to another pool have nothing to check. + */ + default void checkPhysicalDiskForImport(String volumeUuid) { + } + + /** + * Reports whether a disk is in use by a running guest on any host, not only on this one. + * @return null when the pool cannot tell, in which case only the qemu-img lock probe on this host applies + */ + default Boolean isPhysicalDiskInUse(String volumeUuid) { + return null; + } + boolean deletePhysicalDisk(String volumeUuid, Storage.ImageFormat format); List listPhysicalDisks(); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapperTest.java new file mode 100644 index 000000000000..7d7c69974a36 --- /dev/null +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapperTest.java @@ -0,0 +1,154 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CheckVolumeAnswer; +import com.cloud.agent.api.CheckVolumeCommand; +import com.cloud.agent.api.to.StorageFilerTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.storage.Storage; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.HashMap; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public class LibvirtCheckVolumeCommandWrapperTest { + + @Mock + LibvirtComputingResource libvirtComputingResource; + @Mock + KVMStoragePoolManager storagePoolMgr; + @Mock + KVMStoragePool storagePool; + @Mock + StorageFilerTO storageFilerTO; + @Mock + KVMPhysicalDisk disk; + + private final String poolUuid = "pool-uuid"; + private final String volumePath = "1b0a8c3e-5f2d-4c67-9a10-3e5b7d9c2f41"; + private final long virtualSize = 1073741824L; + + LibvirtCheckVolumeCommandWrapper wrapper = new LibvirtCheckVolumeCommandWrapper(); + MockedConstruction qemuImg; + CheckVolumeCommand command; + + @Before + public void setUp() { + Mockito.when(storageFilerTO.getUuid()).thenReturn(poolUuid); + Mockito.when(storageFilerTO.getType()).thenReturn(Storage.StoragePoolType.Linstor); + Mockito.when(libvirtComputingResource.getStoragePoolMgr()).thenReturn(storagePoolMgr); + Mockito.when(storagePoolMgr.getStoragePool(Storage.StoragePoolType.Linstor, poolUuid)).thenReturn(storagePool); + + Map info = new HashMap<>(); + info.put(QemuImg.FILE_FORMAT, "raw"); + qemuImg = Mockito.mockConstruction(QemuImg.class, (mock, context) -> + Mockito.when(mock.info(Mockito.any(QemuImgFile.class), Mockito.anyBoolean())).thenReturn(info)); + + command = new CheckVolumeCommand(); + command.setSrcFile(volumePath); + command.setStorageFilerTO(storageFilerTO); + } + + @After + public void tearDown() { + qemuImg.close(); + } + + private void mockDisk() { + Mockito.when(storagePool.getType()).thenReturn(Storage.StoragePoolType.Linstor); + Mockito.when(storagePool.getPhysicalDisk(volumePath)).thenReturn(disk); + Mockito.when(disk.getPath()).thenReturn("/dev/drbd/by-res/cs-" + volumePath + "/0"); + Mockito.when(disk.getFormat()).thenReturn(QemuImg.PhysicalDiskFormat.RAW); + Mockito.when(disk.getVirtualSize()).thenReturn(virtualSize); + } + + @Test + public void testLinstorDiskInUseOnAnyHostIsReportedLocked() { + Mockito.when(storagePool.isPhysicalDiskInUse(volumePath)).thenReturn(true); + + Answer answer = wrapper.execute(command, libvirtComputingResource); + + Assert.assertTrue(answer instanceof CheckVolumeAnswer); + Assert.assertFalse(answer.getResult()); + Assert.assertEquals("true", ((CheckVolumeAnswer) answer).getVolumeDetails().get(VolumeOnStorageTO.Detail.IS_LOCKED)); + Mockito.verify(storagePool, Mockito.never()).connectPhysicalDisk(Mockito.eq(volumePath), Mockito.any()); + Mockito.verify(storagePool, Mockito.never()).disconnectPhysicalDisk(volumePath); + } + + @Test + public void testLinstorDiskConnectedForTheCheckIsDisconnectedAgain() { + Mockito.when(storagePool.isPhysicalDiskInUse(volumePath)).thenReturn(false); + Mockito.when(storagePool.connectPhysicalDisk(volumePath, null)).thenReturn(true); + mockDisk(); + + Answer answer = wrapper.execute(command, libvirtComputingResource); + + Assert.assertTrue(answer instanceof CheckVolumeAnswer); + Assert.assertTrue(answer.getResult()); + Assert.assertEquals(virtualSize, ((CheckVolumeAnswer) answer).getSize()); + Assert.assertEquals("raw", ((CheckVolumeAnswer) answer).getVolumeDetails().get(VolumeOnStorageTO.Detail.FILE_FORMAT)); + Assert.assertEquals("false", ((CheckVolumeAnswer) answer).getVolumeDetails().get(VolumeOnStorageTO.Detail.IS_LOCKED)); + Mockito.verify(storagePool).checkPhysicalDiskForImport(volumePath); + Mockito.verify(storagePool).disconnectPhysicalDisk(volumePath); + } + + @Test + public void testLinstorDiskIsDisconnectedWhenTheCheckFails() { + Mockito.when(storagePool.isPhysicalDiskInUse(volumePath)).thenReturn(false); + Mockito.when(storagePool.connectPhysicalDisk(volumePath, null)).thenReturn(true); + Mockito.when(storagePool.getPhysicalDisk(volumePath)).thenThrow(new CloudRuntimeException("device not found")); + + Answer answer = wrapper.execute(command, libvirtComputingResource); + + Assert.assertFalse(answer.getResult()); + Mockito.verify(storagePool).disconnectPhysicalDisk(volumePath); + } + + @Test + public void testLinstorDiskOutsideThePoolFails() { + Mockito.when(storagePool.isPhysicalDiskInUse(volumePath)).thenReturn(false); + Mockito.doThrow(new CloudRuntimeException("not in resource group")).when(storagePool).checkPhysicalDiskForImport(volumePath); + + Answer answer = wrapper.execute(command, libvirtComputingResource); + + Assert.assertFalse(answer instanceof CheckVolumeAnswer); + Assert.assertFalse(answer.getResult()); + Mockito.verify(storagePool, Mockito.never()).connectPhysicalDisk(Mockito.eq(volumePath), Mockito.any()); + Mockito.verify(storagePool, Mockito.never()).disconnectPhysicalDisk(volumePath); + } +} diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapperTest.java index 4e039f318928..46163046db54 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVolumesOnStorageCommandWrapperTest.java @@ -28,6 +28,7 @@ import com.cloud.hypervisor.kvm.storage.KVMStoragePool; import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; import com.cloud.storage.Storage; +import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImgFile; @@ -155,4 +156,31 @@ public void testLibvirtGetVolumesOnStorageCommandWrapperForVolume() { Mockito.verify(volumeOnStorageTO).addDetail(VolumeOnStorageTO.Detail.IS_ENCRYPTED, "true"); Mockito.verify(volumeOnStorageTO).addDetail(VolumeOnStorageTO.Detail.IS_LOCKED, "false"); } + + @Test + public void testLibvirtGetVolumesOnStorageCommandWrapperForVolumeOutsideThePool() { + Mockito.doThrow(new CloudRuntimeException("not in resource group")).when(storagePool).checkPhysicalDiskForImport(volumePath); + + GetVolumesOnStorageCommand command = new GetVolumesOnStorageCommand(pool, volumePath, null); + Answer answer = libvirtGetVolumesOnStorageCommandWrapper.execute(command, libvirtComputingResource); + Assert.assertFalse(answer.getResult()); + Assert.assertEquals("not in resource group", answer.getDetails()); + Mockito.verify(storagePool, Mockito.never()).getPhysicalDisk(volumePath); + } + + @Test + public void testLibvirtGetVolumesOnStorageCommandWrapperForVolumeInUseOnAnyHost() { + KVMPhysicalDisk disk = Mockito.mock(KVMPhysicalDisk.class); + Mockito.when(disk.getPath()).thenReturn(volumePath); + Mockito.when(disk.getFormat()).thenReturn(QemuImg.PhysicalDiskFormat.RAW); + Mockito.when(storagePool.getPhysicalDisk(volumePath)).thenReturn(disk); + Mockito.when(storagePool.getType()).thenReturn(poolType); + Mockito.when(storagePool.isPhysicalDiskInUse(volumePath)).thenReturn(true); + + GetVolumesOnStorageCommand command = new GetVolumesOnStorageCommand(pool, volumePath, null); + Answer answer = libvirtGetVolumesOnStorageCommandWrapper.execute(command, libvirtComputingResource); + Assert.assertTrue(answer.getResult()); + VolumeOnStorageTO volumeOnStorageTO = volumeOnStorageTOMock.constructed().get(0); + Mockito.verify(volumeOnStorageTO).addDetail(VolumeOnStorageTO.Detail.IS_LOCKED, "true"); + } } diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java index 848bdc60d664..6450972c8545 100644 --- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptor.java @@ -357,6 +357,51 @@ public boolean connectPhysicalDisk( return true; } + /** + * Checks that a resource which is not managed by CloudStack yet can be imported into this pool. It must + * belong to the resource group of the pool, otherwise a resource owned by a volume of another pool on the + * same controller could be imported a second time. + */ + public void checkPhysicalDiskForImport(String name, LinstorStoragePool pool) { + final DevelopersApi api = getLinstorAPI(pool); + final String rscName = getLinstorRscName(name); + try { + List rscDfns = api.resourceDefinitionList( + Collections.singletonList(rscName), null, null, null, null); + if (rscDfns == null || rscDfns.isEmpty()) { + throw new CloudRuntimeException(String.format("Linstor: resource %s not found", rscName)); + } + final String rscGrpName = rscDfns.get(0).getResourceGroupName(); + if (rscGrpName == null || !rscGrpName.equalsIgnoreCase(pool.getResourceGroup())) { + throw new CloudRuntimeException(String.format( + "Linstor: resource %s belongs to resource group %s, not to %s of this storage pool", + rscName, rscGrpName, pool.getResourceGroup())); + } + } catch (ApiException apiEx) { + logger.error(apiEx); + throw new CloudRuntimeException(apiEx.getBestMessage(), apiEx); + } + } + + /** + * Asks the controller whether the resource is in use (DRBD primary) on any node, which a qemu-img lock + * probe on this node cannot see. + */ + public Boolean isPhysicalDiskInUse(String name, KVMStoragePool pool) { + final DevelopersApi api = getLinstorAPI(pool); + final String rscName = getLinstorRscName(name); + try { + String inUseNode = LinstorUtil.isResourceInUse(api, rscName); + if (inUseNode != null) { + logger.info("Linstor: resource {} is in use on node {}", rscName, inUseNode); + } + return inUseNode != null; + } catch (ApiException apiEx) { + logger.error(apiEx); + throw new CloudRuntimeException(apiEx.getBestMessage(), apiEx); + } + } + private void removeTwoPrimariesRDProps(DevelopersApi api, String rscName, List deleteProps) throws ApiException { ResourceDefinitionModify rdm = new ResourceDefinitionModify(); @@ -572,7 +617,30 @@ public KVMPhysicalDisk createDiskFromTemplate( @Override public List listPhysicalDisks(String storagePoolUuid, KVMStoragePool pool) { - throw new UnsupportedOperationException("Listing disks is not supported for this configuration."); + // lists the resources of the pool's resource group by their volume path, for volume import + final String rscGrpName = ((LinstorStoragePool) pool).getResourceGroup(); + final DevelopersApi api = getLinstorAPI(pool); + try { + List disks = new ArrayList<>(); + for (ResourceDefinition rd : api.resourceDefinitionList(null, true, null, null, null)) { + if (!rd.getName().toLowerCase().startsWith(LinstorUtil.RSC_PREFIX) || rd.getResourceGroupName() == null + || !rd.getResourceGroupName().equalsIgnoreCase(rscGrpName) + || rd.getVolumeDefinitions() == null || rd.getVolumeDefinitions().isEmpty()) { + continue; + } + final KVMPhysicalDisk disk = new KVMPhysicalDisk(LinstorUtil.formatDrbdByResDevicePath(rd.getName()), + rd.getName().substring(LinstorUtil.RSC_PREFIX.length()), pool); + final long size = rd.getVolumeDefinitions().get(0).getSizeKib() * 1024; + disk.setFormat(QemuImg.PhysicalDiskFormat.RAW); + disk.setSize(size); + disk.setVirtualSize(size); + disks.add(disk); + } + return disks; + } catch (ApiException apiEx) { + logger.error(apiEx); + throw new CloudRuntimeException(apiEx.getBestMessage(), apiEx); + } } @Override diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStoragePool.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStoragePool.java index 8a02560fd464..71dc3f46b716 100644 --- a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStoragePool.java +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/storage/LinstorStoragePool.java @@ -92,6 +92,18 @@ public boolean disconnectPhysicalDisk(String volumeUuid) return _storageAdaptor.disconnectPhysicalDisk(volumeUuid, this); } + @Override + public void checkPhysicalDiskForImport(String volumeUuid) + { + ((LinstorStorageAdaptor)_storageAdaptor).checkPhysicalDiskForImport(volumeUuid, this); + } + + @Override + public Boolean isPhysicalDiskInUse(String volumeUuid) + { + return ((LinstorStorageAdaptor)_storageAdaptor).isPhysicalDiskInUse(volumeUuid, this); + } + @Override public boolean deletePhysicalDisk(String volumeUuid, Storage.ImageFormat format) { diff --git a/plugins/storage/volume/linstor/src/test/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptorTest.java b/plugins/storage/volume/linstor/src/test/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptorTest.java new file mode 100644 index 000000000000..bf0efe023463 --- /dev/null +++ b/plugins/storage/volume/linstor/src/test/java/com/cloud/hypervisor/kvm/storage/LinstorStorageAdaptorTest.java @@ -0,0 +1,163 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.hypervisor.kvm.storage; + +import com.cloud.utils.exception.CloudRuntimeException; +import com.linbit.linstor.api.ApiClient; +import com.linbit.linstor.api.ApiConsts; +import com.linbit.linstor.api.DevelopersApi; +import com.linbit.linstor.api.model.Resource; +import com.linbit.linstor.api.model.ResourceDefinition; +import com.linbit.linstor.api.model.ResourceState; +import com.linbit.linstor.api.model.VolumeDefinition; + +import org.apache.cloudstack.utils.qemu.QemuImg; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class LinstorStorageAdaptorTest { + + private static final String LOCAL_NODE = "node1"; + private static final String VOLUME = "1b0a8c3e-5f2d-4c67-9a10-3e5b7d9c2f41"; + private static final String RSC_NAME = "cs-" + VOLUME; + private static final String RSC_GROUP = "Linstor-RG"; + + private LinstorStorageAdaptor adaptor; + private LinstorStoragePool pool; + private MockedConstruction apiClients; + private MockedConstruction apis; + private final List rscDfns = new ArrayList<>(); + private final List resources = new ArrayList<>(); + + @Before + public void setUp() { + try (MockedStatic ignored = Mockito.mockStatic(LinstorStoragePool.class)) { + Mockito.when(LinstorStoragePool.getHostname()).thenReturn(LOCAL_NODE); + adaptor = new LinstorStorageAdaptor(); + } + pool = Mockito.mock(LinstorStoragePool.class); + Mockito.lenient().when(pool.getResourceGroup()).thenReturn(RSC_GROUP); + + apiClients = Mockito.mockConstruction(ApiClient.class); + apis = Mockito.mockConstruction(DevelopersApi.class, (api, context) -> { + Mockito.lenient().when(api.resourceDefinitionList(Collections.singletonList(RSC_NAME), null, null, null, null)) + .thenReturn(rscDfns); + Mockito.lenient().when(api.resourceList(RSC_NAME, null, null)).thenReturn(resources); + Mockito.lenient().when(api.resourceDefinitionList(null, true, null, null, null)).thenReturn(rscDfns); + }); + } + + @After + public void tearDown() { + apis.close(); + apiClients.close(); + } + + private void addResourceDefinition(String rscGroup) { + ResourceDefinition rd = new ResourceDefinition(); + rd.setName(RSC_NAME); + rd.setResourceGroupName(rscGroup); + rscDfns.add(rd); + } + + private void addResource(String node, String... flags) { + Resource rsc = new Resource(); + rsc.setName(RSC_NAME); + rsc.setNodeName(node); + List rscFlags = new ArrayList<>(); + Collections.addAll(rscFlags, flags); + rsc.setFlags(rscFlags); + resources.add(rsc); + } + + @Test + public void checkForImportAcceptsResourceOfPoolGroup() { + addResourceDefinition(RSC_GROUP.toLowerCase()); + + adaptor.checkPhysicalDiskForImport(VOLUME, pool); + } + + @Test + public void checkForImportRejectsResourceOfAnotherResourceGroup() { + addResourceDefinition("OtherRG"); + + Assert.assertThrows(CloudRuntimeException.class, () -> adaptor.checkPhysicalDiskForImport(VOLUME, pool)); + } + + @Test + public void checkForImportRejectsMissingResource() { + Assert.assertThrows(CloudRuntimeException.class, () -> adaptor.checkPhysicalDiskForImport(VOLUME, pool)); + } + + @Test + public void inUseWhenPrimaryOnAnyNode() { + addResource("node2"); + Resource inUse = resources.get(0); + inUse.setState(new ResourceState().inUse(true)); + + Assert.assertTrue(adaptor.isPhysicalDiskInUse(VOLUME, pool)); + } + + @Test + public void notInUseWhenNoNodeIsPrimary() { + addResource("node2"); + addResource(LOCAL_NODE, ApiConsts.FLAG_DRBD_DISKLESS); + + Assert.assertFalse(adaptor.isPhysicalDiskInUse(VOLUME, pool)); + } + + private ResourceDefinition resourceDefinition(String name, String rscGroup, Long sizeKib) { + ResourceDefinition rd = new ResourceDefinition(); + rd.setName(name); + rd.setResourceGroupName(rscGroup); + if (sizeKib != null) { + VolumeDefinition vd = new VolumeDefinition(); + vd.setSizeKib(sizeKib); + rd.setVolumeDefinitions(Collections.singletonList(vd)); + } + return rd; + } + + @Test + public void listPhysicalDisksReturnsResourcesOfPoolGroupByVolumePath() { + rscDfns.add(resourceDefinition(RSC_NAME, RSC_GROUP.toLowerCase(), 1048576L)); + rscDfns.add(resourceDefinition("cs-other-group", "OtherRG", 1048576L)); + rscDfns.add(resourceDefinition("not-cloudstack", RSC_GROUP, 1048576L)); + rscDfns.add(resourceDefinition("cs-no-volume", RSC_GROUP, null)); + + List disks = adaptor.listPhysicalDisks("pool-uuid", pool); + + Assert.assertEquals(1, disks.size()); + Assert.assertEquals(VOLUME, disks.get(0).getName()); + Assert.assertEquals("/dev/drbd/by-res/" + RSC_NAME + "/0", disks.get(0).getPath()); + Assert.assertEquals(1073741824L, disks.get(0).getVirtualSize()); + Assert.assertEquals(QemuImg.PhysicalDiskFormat.RAW, disks.get(0).getFormat()); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java b/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java index 4a69e32e77e4..2b0039d3e04f 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java @@ -459,9 +459,11 @@ private DiskOfferingVO getOrCreateDefaultDiskOfferingIdForVolumeImport(boolean i private VolumeVO importVolumeInternal(VolumeOnStorageTO volume, DiskOfferingVO diskOffering, Account owner, StoragePoolVO pool, String volumeName) { + // as for importVm, a Linstor volume keeps the hypervisor default format like a native Linstor volume + Storage.ImageFormat format = Storage.StoragePoolType.Linstor == pool.getPoolType() ? null : getImageFormat(volume.getFormat()); DiskProfile diskProfile = volumeManager.importVolume(Volume.Type.DATADISK, volumeName, diskOffering, volume.getVirtualSize(), null, null, pool.getDataCenterId(), volume.getHypervisorType(), null, null, - owner, null, pool.getId(), pool.getPoolType(), volume.getPath(), null, getImageFormat(volume.getFormat())); + owner, null, pool.getId(), pool.getPoolType(), volume.getPath(), null, format); return volumeDao.findById(diskProfile.getVolumeId()); } diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 585f00d80eb3..cfac8739304a 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -3006,6 +3006,11 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, try { long deviceId = 1L; Storage.ImageFormat diskFormat = getImageFormatFromVolumeDetails(checkVolumeAnswer.getVolumeDetails()); + // Linstor records its raw volumes with the hypervisor default format, and the templates made from + // their (qcow2) snapshot backups take that format, so keep an imported volume like a native one + if (Storage.StoragePoolType.Linstor == storagePool.getPoolType()) { + diskFormat = null; + } if(ImportSource.SHARED == importSource) { diskProfileStoragePoolList.add(importKVMSharedDisk(userVm, diskOffering, Volume.Type.ROOT, template, deviceId, poolId, diskPath, diskProfile, diskFormat)); diff --git a/server/src/test/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImplTest.java index 08bcaeaced3c..2dd2a9228e52 100644 --- a/server/src/test/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImplTest.java @@ -249,6 +249,17 @@ public void testListVolumesForImport() { @Test public void testImportVolumeAllGood() throws ResourceAllocationException { + importVolumeAndCheckFormat(Storage.ImageFormat.QCOW2); + } + + @Test + public void testImportVolumeOnLinstorPoolKeepsDefaultFormat() throws ResourceAllocationException { + // as for importVm, a Linstor volume keeps the hypervisor default format like a native Linstor volume + when(storagePoolVO.getPoolType()).thenReturn(Storage.StoragePoolType.Linstor); + importVolumeAndCheckFormat(null); + } + + private void importVolumeAndCheckFormat(Storage.ImageFormat expectedFormat) throws ResourceAllocationException { ImportVolumeCmd cmd = mock(ImportVolumeCmd.class); when(cmd.getPath()).thenReturn(path); when(cmd.getStorageId()).thenReturn(poolId); @@ -274,7 +285,7 @@ public void testImportVolumeAllGood() throws ResourceAllocationException { doNothing().when(volumeApiService).validateCustomDiskOfferingSizeRange(anyLong()); doReturn(true).when(volumeApiService).doesStoragePoolSupportDiskOffering(any(), any()); doReturn(diskProfile).when(volumeManager).importVolume(any(), anyString(), any(), eq(virtualSize), isNull(), isNull(), anyLong(), - any(), isNull(), isNull(), any(), isNull(), anyLong(), any(), anyString(), isNull(), eq(Storage.ImageFormat.QCOW2)); + any(), isNull(), isNull(), any(), isNull(), anyLong(), any(), anyString(), isNull(), eq(expectedFormat)); when(diskProfile.getVolumeId()).thenReturn(volumeId); when(volumeDao.findById(volumeId)).thenReturn(volumeVO); diff --git a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java index 30bc769d5d4f..46e727be00bc 100644 --- a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java @@ -961,6 +961,12 @@ public void importFromsharedStorageOnRbdPool() throws InsufficientServerCapacity importFromDisk("shared", Storage.StoragePoolType.RBD, "cloudstack-image", "raw", Storage.ImageFormat.RAW); } + @Test + public void importFromsharedStorageOnLinstorPool() throws InsufficientServerCapacityException { + // the volume records the hypervisor default format like a natively created Linstor volume + importFromDisk("shared", Storage.StoragePoolType.Linstor, "1b0a8c3e-5f2d-4c67-9a10-3e5b7d9c2f41", "raw", null); + } + @Test public void importFromsharedStorageOnZoneWidePool() throws InsufficientServerCapacityException { // A zone wide pool carries no pod or cluster, so the plan must stay unconstrained and the