diff --git a/client/pom.xml b/client/pom.xml index 22cca63e0c8..de9e910b978 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -837,6 +837,12 @@ false ${project.build.directory}/lib + + com.linbit.linstor.api + java-linstor + false + ${project.build.directory}/lib + org.bouncycastle bctls-jdk15on @@ -880,6 +886,7 @@ mysql:mysql-connector-java org.apache.cloudstack:cloud-plugin-storage-volume-storpool org.apache.cloudstack:cloud-plugin-storage-volume-linstor + com.linbit.linstor.api:java-linstor diff --git a/plugins/storage/volume/linstor/pom.xml b/plugins/storage/volume/linstor/pom.xml index 69de4ff3d67..44e79dbce51 100644 --- a/plugins/storage/volume/linstor/pom.xml +++ b/plugins/storage/volume/linstor/pom.xml @@ -43,6 +43,12 @@ cloud-plugin-hypervisor-kvm ${project.version} + + org.apache.cloudstack + cloud-engine-storage-snapshot + ${project.version} + compile + diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/api/storage/LinstorBackupSnapshotCommand.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/api/storage/LinstorBackupSnapshotCommand.java new file mode 100644 index 00000000000..8d887dbba21 --- /dev/null +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/api/storage/LinstorBackupSnapshotCommand.java @@ -0,0 +1,28 @@ +// 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.api.storage; + +import com.cloud.agent.api.to.DataTO; +import org.apache.cloudstack.storage.command.CopyCommand; + +public class LinstorBackupSnapshotCommand extends CopyCommand +{ + public LinstorBackupSnapshotCommand(DataTO srcData, DataTO destData, int timeout, boolean executeInSequence) + { + super(srcData, destData, timeout, executeInSequence); + } +} diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/api/storage/LinstorRevertBackupSnapshotCommand.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/api/storage/LinstorRevertBackupSnapshotCommand.java new file mode 100644 index 00000000000..1f1880d5da5 --- /dev/null +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/api/storage/LinstorRevertBackupSnapshotCommand.java @@ -0,0 +1,28 @@ +// 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.api.storage; + +import com.cloud.agent.api.to.DataTO; +import org.apache.cloudstack.storage.command.CopyCommand; + +public class LinstorRevertBackupSnapshotCommand extends CopyCommand +{ + public LinstorRevertBackupSnapshotCommand(DataTO srcData, DataTO destData, int timeout, boolean executeInSequence) + { + super(srcData, destData, timeout, executeInSequence); + } +} diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorBackupSnapshotCommandWrapper.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorBackupSnapshotCommandWrapper.java new file mode 100644 index 00000000000..a210d53d7e7 --- /dev/null +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorBackupSnapshotCommandWrapper.java @@ -0,0 +1,167 @@ +// 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 java.io.File; +import java.io.IOException; + +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.NfsTO; +import com.cloud.api.storage.LinstorBackupSnapshotCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; +import com.cloud.utils.script.Script; +import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.commons.io.FileUtils; +import org.apache.log4j.Logger; +import org.joda.time.Duration; +import org.libvirt.LibvirtException; + +@ResourceWrapper(handles = LinstorBackupSnapshotCommand.class) +public final class LinstorBackupSnapshotCommandWrapper + extends CommandWrapper +{ + private static final Logger s_logger = Logger.getLogger(LinstorBackupSnapshotCommandWrapper.class); + + private String zfsSnapdev(boolean hide, String zfsUrl) { + Script script = new Script("/usr/bin/zfs", Duration.millis(5000)); + script.add("set"); + script.add("snapdev=" + (hide ? "hidden" : "visible")); + script.add(zfsUrl.substring(6)); // cutting zfs:// + return script.execute(); + } + + private String qemuShrink(String path, long sizeByte, long timeout) { + Script qemuImg = new Script("qemu-img", Duration.millis(timeout)); + qemuImg.add("resize"); + qemuImg.add("--shrink"); + qemuImg.add(path); + qemuImg.add("" + sizeByte); + return qemuImg.execute(); + } + + static void cleanupSecondaryPool(final KVMStoragePool secondaryPool) { + if (secondaryPool != null) { + try { + secondaryPool.delete(); + } catch (final Exception e) { + s_logger.debug("Failed to delete secondary storage", e); + } + } + } + + private String convertImageToQCow2( + final String srcPath, + final SnapshotObjectTO dst, + final KVMStoragePool secondaryPool, + int waitMilliSeconds + ) + throws LibvirtException, QemuImgException, IOException + { + final String dstDir = secondaryPool.getLocalPath() + File.separator + dst.getPath(); + FileUtils.forceMkdir(new File(dstDir)); + + final String dstPath = dstDir + File.separator + dst.getName(); + final QemuImgFile srcFile = new QemuImgFile(srcPath, QemuImg.PhysicalDiskFormat.RAW); + final QemuImgFile dstFile = new QemuImgFile(dstPath, QemuImg.PhysicalDiskFormat.QCOW2); + + // NOTE: the qemu img will also contain the drbd metadata at the end + final QemuImg qemu = new QemuImg(waitMilliSeconds); + qemu.convert(srcFile, dstFile); + s_logger.info("Backup snapshot " + srcFile + " to " + dstPath); + return dstPath; + } + + private SnapshotObjectTO setCorrectSnapshotSize(final SnapshotObjectTO dst, final String dstPath) { + final File snapFile = new File(dstPath); + final long size = snapFile.exists() ? snapFile.length() : 0; + + final SnapshotObjectTO snapshot = new SnapshotObjectTO(); + snapshot.setPath(dst.getPath() + File.separator + dst.getName()); + snapshot.setPhysicalSize(size); + return snapshot; + } + + @Override + public CopyCmdAnswer execute(LinstorBackupSnapshotCommand cmd, LibvirtComputingResource serverResource) + { + s_logger.debug("LinstorBackupSnapshotCommandWrapper: " + cmd.getSrcTO().getPath() + " -> " + cmd.getDestTO().getPath()); + final SnapshotObjectTO src = (SnapshotObjectTO) cmd.getSrcTO(); + final SnapshotObjectTO dst = (SnapshotObjectTO) cmd.getDestTO(); + KVMStoragePool secondaryPool = null; + final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); + KVMStoragePool linstorPool = storagePoolMgr.getStoragePool(Storage.StoragePoolType.Linstor, src.getDataStore().getUuid()); + boolean zfsHidden = false; + String srcPath = src.getPath(); + + if (linstorPool == null) { + return new CopyCmdAnswer("Unable to get linstor storage pool from destination volume."); + } + + final DataStoreTO dstDataStore = dst.getDataStore(); + if (!(dstDataStore instanceof NfsTO)) { + return new CopyCmdAnswer("Backup Linstor snapshot: Only NFS secondary supported at present!"); + } + + try + { + // provide the linstor snapshot block device + // on lvm thin this should already be there in /dev/mapper/vg-snapshotname + // on zfs we need to unhide the snapshot block device + s_logger.info("Src: " + srcPath + " | " + src.getName()); + if (srcPath.startsWith("zfs://")) { + zfsHidden = true; + if (zfsSnapdev(false, srcPath) != null) { + return new CopyCmdAnswer("Unable to unhide zfs snapshot device."); + } + srcPath = "/dev/" + srcPath.substring(6); + } + + secondaryPool = storagePoolMgr.getStoragePoolByURI(dstDataStore.getUrl()); + + String dstPath = convertImageToQCow2(srcPath, dst, secondaryPool, cmd.getWaitInMillSeconds()); + + // resize to real volume size, cutting of drbd metadata + String result = qemuShrink(dstPath, src.getVolume().getSize(), cmd.getWaitInMillSeconds()); + if (result != null) { + return new CopyCmdAnswer("qemu-img shrink failed: " + result); + } + s_logger.info("Backup shrunk " + dstPath + " to actual size " + src.getVolume().getSize()); + + SnapshotObjectTO snapshot = setCorrectSnapshotSize(dst, dstPath); + return new CopyCmdAnswer(snapshot); + } catch (final Exception e) { + final String error = String.format("Failed to backup snapshot with id [%s] with a pool %s, due to %s", + cmd.getSrcTO().getId(), cmd.getSrcTO().getDataStore().getUuid(), e.getMessage()); + s_logger.error(error); + return new CopyCmdAnswer(cmd, e); + } finally { + cleanupSecondaryPool(secondaryPool); + if (zfsHidden) { + zfsSnapdev(true, src.getPath()); + } + } + } +} diff --git a/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java new file mode 100644 index 00000000000..511b5a40ca8 --- /dev/null +++ b/plugins/storage/volume/linstor/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LinstorRevertBackupSnapshotCommandWrapper.java @@ -0,0 +1,92 @@ +// 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 java.io.File; + +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.api.storage.LinstorRevertBackupSnapshotCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; +import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.log4j.Logger; +import org.libvirt.LibvirtException; + +@ResourceWrapper(handles = LinstorRevertBackupSnapshotCommand.class) +public final class LinstorRevertBackupSnapshotCommandWrapper + extends CommandWrapper +{ + private static final Logger s_logger = Logger.getLogger(LinstorRevertBackupSnapshotCommandWrapper.class); + + private void convertQCow2ToRAW(final String srcPath, final String dstPath, int waitMilliSeconds) + throws LibvirtException, QemuImgException + { + final QemuImgFile srcQemuFile = new QemuImgFile( + srcPath, QemuImg.PhysicalDiskFormat.QCOW2); + final QemuImg qemu = new QemuImg(waitMilliSeconds); + final QemuImgFile dstFile = new QemuImgFile(dstPath, QemuImg.PhysicalDiskFormat.RAW); + qemu.convert(srcQemuFile, dstFile); + } + + @Override + public CopyCmdAnswer execute(LinstorRevertBackupSnapshotCommand cmd, LibvirtComputingResource serverResource) + { + s_logger.debug("LinstorRevertBackupSnapshotCommandWrapper: " + cmd.getSrcTO().getPath() + " -> " + cmd.getDestTO().getPath()); + final SnapshotObjectTO src = (SnapshotObjectTO) cmd.getSrcTO(); + final VolumeObjectTO dst = (VolumeObjectTO) cmd.getDestTO(); + KVMStoragePool secondaryPool = null; + final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); + KVMStoragePool linstorPool = storagePoolMgr.getStoragePool(Storage.StoragePoolType.Linstor, dst.getDataStore().getUuid()); + + if (linstorPool == null) { + return new CopyCmdAnswer("Unable to get linstor storage pool from destination volume."); + } + + try + { + final DataStoreTO srcDataStore = src.getDataStore(); + File srcFile = new File(src.getPath()); + secondaryPool = storagePoolMgr.getStoragePoolByURI( + srcDataStore.getUrl() + File.separator + srcFile.getParent()); + + convertQCow2ToRAW( + secondaryPool.getLocalPath() + File.separator + srcFile.getName(), + linstorPool.getPhysicalDisk(dst.getPath()).getPath(), + cmd.getWaitInMillSeconds()); + + final VolumeObjectTO dstVolume = new VolumeObjectTO(); + dstVolume.setPath(dst.getPath()); + return new CopyCmdAnswer(dstVolume); + } catch (final Exception e) { + final String error = String.format("Failed to revert snapshot with id [%s] with a pool %s, due to %s", + cmd.getSrcTO().getId(), cmd.getSrcTO().getDataStore().getUuid(), e.getMessage()); + s_logger.error(error); + return new CopyCmdAnswer(cmd, e); + } finally { + LinstorBackupSnapshotCommandWrapper.cleanupSecondaryPool(secondaryPool); + } + } +} 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 2847484e30e..92b617c7128 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 @@ -28,6 +28,7 @@ import java.util.StringJoiner; import javax.annotation.Nonnull; +import org.apache.cloudstack.storage.datastore.util.LinstorUtil; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImgException; import org.apache.cloudstack.utils.qemu.QemuImgFile; @@ -65,8 +66,8 @@ public class LinstorStorageAdaptor implements StorageAdaptor { return new DevelopersApi(client); } - private String getLinstorRscName(String name) { - return "cs-" + name; + private static String getLinstorRscName(String name) { + return LinstorUtil.RSC_PREFIX + name; } private String getHostname() { @@ -214,6 +215,7 @@ public class LinstorStorageAdaptor implements StorageAdaptor { public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, QemuImg.PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase) { + s_logger.debug(String.format("Linstor.createPhysicalDisk: %s;%s", name, format)); final String rscName = getLinstorRscName(name); LinstorStoragePool lpool = (LinstorStoragePool) pool; final DevelopersApi api = getLinstorAPI(pool); @@ -254,6 +256,7 @@ public class LinstorStorageAdaptor implements StorageAdaptor { throw new CloudRuntimeException("Linstor: viewResources didn't return resources or volumes."); } } catch (ApiException apiEx) { + s_logger.error(String.format("Linstor.createPhysicalDisk: ApiException: %s", apiEx.getBestMessage())); throw new CloudRuntimeException(apiEx.getBestMessage(), apiEx); } } @@ -424,7 +427,7 @@ public class LinstorStorageAdaptor implements StorageAdaptor { @Override public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMStoragePool destPools, int timeout, byte[] srcPassphrase, byte[] destPassphrase, Storage.ProvisioningType provisioningType) { - s_logger.debug("Linstor: copyPhysicalDisk"); + s_logger.debug(String.format("Linstor.copyPhysicalDisk: %s -> %s", disk.getPath(), name)); final QemuImg.PhysicalDiskFormat sourceFormat = disk.getFormat(); final String sourcePath = disk.getPath(); @@ -433,6 +436,7 @@ public class LinstorStorageAdaptor implements StorageAdaptor { final KVMPhysicalDisk dstDisk = destPools.createPhysicalDisk( name, QemuImg.PhysicalDiskFormat.RAW, provisioningType, disk.getVirtualSize(), null); + s_logger.debug(String.format("Linstor.copyPhysicalDisk: dstPath: %s", dstDisk.getPath())); final QemuImgFile destFile = new QemuImgFile(dstDisk.getPath()); destFile.setFormat(dstDisk.getFormat()); destFile.setSize(disk.getVirtualSize()); 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 d67b8ab7d5b..c0f3cb4b459 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 @@ -28,6 +28,7 @@ import com.linbit.linstor.api.model.ResourceDefinitionCloneStarted; import com.linbit.linstor.api.model.ResourceDefinitionCreate; import com.linbit.linstor.api.model.ResourceDefinitionModify; import com.linbit.linstor.api.model.ResourceGroupSpawn; +import com.linbit.linstor.api.model.ResourceMakeAvailable; import com.linbit.linstor.api.model.ResourceWithVolumes; import com.linbit.linstor.api.model.Snapshot; import com.linbit.linstor.api.model.SnapshotRestore; @@ -43,18 +44,26 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import com.cloud.agent.api.Answer; import com.cloud.agent.api.storage.ResizeVolumeAnswer; import com.cloud.agent.api.storage.ResizeVolumeCommand; +import com.cloud.agent.api.to.DataObjectType; import com.cloud.agent.api.to.DataStoreTO; import com.cloud.agent.api.to.DataTO; import com.cloud.agent.api.to.DiskTO; import com.cloud.agent.api.to.StorageFilerTO; +import com.cloud.api.storage.LinstorBackupSnapshotCommand; +import com.cloud.api.storage.LinstorRevertBackupSnapshotCommand; +import com.cloud.configuration.Config; import com.cloud.host.Host; +import com.cloud.host.dao.HostDao; +import com.cloud.storage.DataStoreRole; import com.cloud.storage.ResizeVolumePayload; import com.cloud.storage.SnapshotVO; import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.Storage; import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; import com.cloud.storage.VMTemplateStoragePoolVO; @@ -67,8 +76,10 @@ import com.cloud.storage.dao.SnapshotDetailsVO; import com.cloud.storage.dao.VMTemplatePoolDao; import com.cloud.storage.dao.VolumeDao; import com.cloud.storage.dao.VolumeDetailsDao; +import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VirtualMachineManager; import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult; @@ -80,10 +91,14 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo; import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.framework.async.AsyncCompletionCallback; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.storage.RemoteHostEndPoint; import org.apache.cloudstack.storage.command.CommandResult; +import org.apache.cloudstack.storage.command.CopyCommand; import org.apache.cloudstack.storage.command.CreateObjectAnswer; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; 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.volume.VolumeObject; @@ -98,6 +113,10 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver @Inject private SnapshotDao _snapshotDao; @Inject private SnapshotDetailsDao _snapshotDetailsDao; @Inject private StorageManager _storageMgr; + @Inject + ConfigurationDao _configDao; + @Inject + private HostDao _hostDao; public LinstorPrimaryDataStoreDriverImpl() { @@ -109,10 +128,12 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver Map mapCapabilities = new HashMap<>(); // Linstor will be restricted to only run on LVM-THIN and ZFS storage pools with ACS + // This enables template caching on our primary storage mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_VOLUME.toString(), Boolean.TRUE.toString()); // fetch if lvm-thin or ZFS - mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.TRUE.toString()); + boolean system_snapshot = !LinstorConfigurationManager.BackupSnapshots.value(); + mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.toString(system_snapshot)); // CAN_CREATE_VOLUME_FROM_SNAPSHOT see note from CAN_CREATE_VOLUME_FROM_VOLUME mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.TRUE.toString()); @@ -216,6 +237,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver } throw new CloudRuntimeException("Linstor: Unable to delete snapshot: " + rscDefName); } + s_logger.info("Linstor: Deleted snapshot " + snapshotName + " for resource " + rscDefName); } catch (ApiException apiEx) { s_logger.error("Linstor: ApiEx - " + apiEx.getMessage()); @@ -402,27 +424,46 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver } } - private String createResource(VolumeInfo vol, StoragePoolVO storagePoolVO) - { - DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePoolVO.getHostAddress()); - final String rscGrp = storagePoolVO.getUserInfo() != null && !storagePoolVO.getUserInfo().isEmpty() ? + private String getRscGrp(StoragePoolVO storagePoolVO) { + return storagePoolVO.getUserInfo() != null && !storagePoolVO.getUserInfo().isEmpty() ? storagePoolVO.getUserInfo() : "DfltRscGrp"; + } + private String createResourceBase( + String rscName, long sizeInBytes, String volName, String vmName, DevelopersApi api, String rscGrp) { ResourceGroupSpawn rscGrpSpawn = new ResourceGroupSpawn(); - final String rscName = LinstorUtil.RSC_PREFIX + vol.getUuid(); rscGrpSpawn.setResourceDefinitionName(rscName); - rscGrpSpawn.addVolumeSizesItem(vol.getSize() / 1024); + rscGrpSpawn.addVolumeSizesItem(sizeInBytes / 1024); try { s_logger.info("Linstor: Spawn resource " + rscName); - ApiCallRcList answers = linstorApi.resourceGroupSpawn(rscGrp, rscGrpSpawn); + ApiCallRcList answers = api.resourceGroupSpawn(rscGrp, rscGrpSpawn); checkLinstorAnswersThrow(answers); - applyAuxProps(linstorApi, rscName, vol.getName(), vol.getAttachedVmName()); + applyAuxProps(api, rscName, volName, vmName); + + return getDeviceName(api, rscName); + } catch (ApiException apiEx) + { + s_logger.error("Linstor: ApiEx - " + apiEx.getMessage()); + throw new CloudRuntimeException(apiEx.getBestMessage(), apiEx); + } + } + + private String createResource(VolumeInfo vol, StoragePoolVO storagePoolVO) { + DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePoolVO.getHostAddress()); + final String rscGrp = getRscGrp(storagePoolVO); + + final String rscName = LinstorUtil.RSC_PREFIX + vol.getUuid(); + String deviceName = createResourceBase( + rscName, vol.getSize(), vol.getName(), vol.getAttachedVmName(), linstorApi, rscGrp); + + try + { applyQoSSettings(storagePoolVO, linstorApi, rscName, vol.getMaxIops()); - return getDeviceName(linstorApi, rscName); + return deviceName; } catch (ApiException apiEx) { s_logger.error("Linstor: ApiEx - " + apiEx.getMessage()); @@ -487,8 +528,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver } private String createResourceFromSnapshot(long csSnapshotId, String rscName, StoragePoolVO storagePoolVO) { - final String rscGrp = storagePoolVO.getUserInfo() != null && !storagePoolVO.getUserInfo().isEmpty() ? - storagePoolVO.getUserInfo() : "DfltRscGrp"; + final String rscGrp = getRscGrp(storagePoolVO); final DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(storagePoolVO.getHostAddress()); SnapshotVO snapshotVO = _snapshotDao.findById(csSnapshotId); @@ -654,6 +694,59 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver } } + private String revertSnapshotFromImageStore( + final SnapshotInfo snapshot, + final VolumeInfo volumeInfo, + final DevelopersApi linstorApi, + final String rscName) + throws ApiException { + String resultMsg = null; + String value = _configDao.getValue(Config.BackupSnapshotWait.toString()); + int _backupsnapshotwait = NumbersUtil.parseInt( + value, Integer.parseInt(Config.BackupSnapshotWait.getDefaultValue())); + + LinstorRevertBackupSnapshotCommand cmd = new LinstorRevertBackupSnapshotCommand( + snapshot.getTO(), + volumeInfo.getTO(), + _backupsnapshotwait, + VirtualMachineManager.ExecuteInSequence.value()); + + Optional optEP = getDiskfullEP(linstorApi, rscName); + if (optEP.isPresent()) { + Answer answer = optEP.get().sendMessage(cmd); + if (!answer.getResult()) { + resultMsg = answer.getDetails(); + } + } else { + resultMsg = "Unable to get matching Linstor endpoint."; + } + return resultMsg; + } + + private String doRevertSnapshot(final SnapshotInfo snapshot, final VolumeInfo volumeInfo) { + final StoragePool pool = (StoragePool) volumeInfo.getDataStore(); + final DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(pool.getHostAddress()); + final String rscName = LinstorUtil.RSC_PREFIX + volumeInfo.getUuid(); + String resultMsg; + try { + if (snapshot.getDataStore().getRole() == DataStoreRole.Primary) { + final String snapName = LinstorUtil.RSC_PREFIX + snapshot.getUuid(); + + ApiCallRcList answers = linstorApi.resourceSnapshotRollback(rscName, snapName); + resultMsg = checkLinstorAnswers(answers); + } else if (snapshot.getDataStore().getRole() == DataStoreRole.Image) { + resultMsg = revertSnapshotFromImageStore(snapshot, volumeInfo, linstorApi, rscName); + } else { + resultMsg = "Linstor: Snapshot revert datastore not supported"; + } + } catch (ApiException apiEx) { + s_logger.error("Linstor: ApiEx - " + apiEx.getMessage()); + resultMsg = apiEx.getBestMessage(); + } + + return resultMsg; + } + @Override public void revertSnapshot( SnapshotInfo snapshot, @@ -670,19 +763,7 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver return; } - String resultMsg; - try { - final StoragePool pool = (StoragePool) snapshot.getDataStore(); - final String rscName = LinstorUtil.RSC_PREFIX + volumeInfo.getUuid(); - final String snapName = LinstorUtil.RSC_PREFIX + snapshot.getUuid(); - final DevelopersApi linstorApi = LinstorUtil.getLinstorAPI(pool.getHostAddress()); - - ApiCallRcList answers = linstorApi.resourceSnapshotRollback(rscName, snapName); - resultMsg = checkLinstorAnswers(answers); - } catch (ApiException apiEx) { - s_logger.error("Linstor: ApiEx - " + apiEx.getMessage()); - resultMsg = apiEx.getBestMessage(); - } + String resultMsg = doRevertSnapshot(snapshot, volumeInfo); if (callback != null) { @@ -692,24 +773,211 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver } } + private static boolean canCopySnapshotCond(DataObject srcData, DataObject dstData) { + return srcData.getType() == DataObjectType.SNAPSHOT && dstData.getType() == DataObjectType.SNAPSHOT + && (dstData.getDataStore().getRole() == DataStoreRole.Image + || dstData.getDataStore().getRole() == DataStoreRole.ImageCache); + } + + private static boolean canCopyTemplateCond(DataObject srcData, DataObject dstData) { + return srcData.getType() == DataObjectType.TEMPLATE && dstData.getType() == DataObjectType.TEMPLATE + && dstData.getDataStore().getRole() == DataStoreRole.Primary + && (srcData.getDataStore().getRole() == DataStoreRole.Image + || srcData.getDataStore().getRole() == DataStoreRole.ImageCache); + } + @Override - public boolean canCopy(DataObject srcData, DataObject destData) + public boolean canCopy(DataObject srcData, DataObject dstData) { + s_logger.debug("LinstorPrimaryDataStoreDriverImpl.canCopy: " + srcData.getType() + " -> " + dstData.getType()); + + if (canCopySnapshotCond(srcData, dstData)) { + SnapshotInfo sinfo = (SnapshotInfo) srcData; + VolumeInfo volume = sinfo.getBaseVolume(); + StoragePoolVO storagePool = _storagePoolDao.findById(volume.getPoolId()); + return storagePool.getStorageProviderName().equals(LinstorUtil.PROVIDER_NAME); + } else if (canCopyTemplateCond(srcData, dstData)) { + TemplateInfo tInfo = (TemplateInfo) dstData; + StoragePoolVO storagePoolVO = _storagePoolDao.findById(dstData.getDataStore().getId()); + return storagePoolVO != null + && storagePoolVO.getPoolType() == Storage.StoragePoolType.Linstor + && tInfo.getSize() != null; + } return false; } @Override - public void copyAsync(DataObject srcData, DataObject destData, AsyncCompletionCallback callback) + public void copyAsync(DataObject srcData, DataObject dstData, AsyncCompletionCallback callback) { - // as long as canCopy is false, this isn't called - s_logger.debug("Linstor: copyAsync with srcdata: " + srcData.getUuid()); + s_logger.debug("LinstorPrimaryDataStoreDriverImpl.copyAsync: " + + srcData.getType() + " -> " + dstData.getType()); + + final CopyCommandResult res; + if (canCopySnapshotCond(srcData, dstData)) { + String errMsg = null; + Answer answer = copySnapshot(srcData, dstData); + if (answer != null && !answer.getResult()) { + errMsg = answer.getDetails(); + } else { + // delete primary storage snapshot + SnapshotInfo sinfo = (SnapshotInfo) srcData; + VolumeInfo volume = sinfo.getBaseVolume(); + deleteSnapshot( + srcData.getDataStore(), + LinstorUtil.RSC_PREFIX + volume.getUuid(), + LinstorUtil.RSC_PREFIX + sinfo.getUuid()); + } + res = new CopyCommandResult(null, answer); + res.setResult(errMsg); + } else if (canCopyTemplateCond(srcData, dstData)) { + Answer answer = copyTemplate(srcData, dstData); + res = new CopyCommandResult(null, answer); + } else { + Answer answer = new Answer(null, false, "noimpl"); + res = new CopyCommandResult(null, answer); + res.setResult("Not implemented yet"); + } + callback.complete(res); + } + + private Optional getLinstorEP(DevelopersApi api, String rscName) throws ApiException { + List linstorNodeNames = LinstorUtil.getLinstorNodeNames(api); + Collections.shuffle(linstorNodeNames); // do not always pick the first linstor node + + Host host = null; + for (String nodeName : linstorNodeNames) { + host = _hostDao.findByName(nodeName); + if (host != null) { + s_logger.info(String.format("Linstor: Make resource %s available on node %s ...", rscName, nodeName)); + ApiCallRcList answers = api.resourceMakeAvailableOnNode(rscName, nodeName, new ResourceMakeAvailable()); + if (!answers.hasError()) { + break; // found working host + } else { + s_logger.error( + String.format("Linstor: Unable to make resource %s on node %s available: %s", + rscName, + nodeName, + LinstorUtil.getBestErrorMessage(answers))); + } + } + } + + if (host == null) + { + s_logger.error("Linstor: Couldn't create a resource on any cloudstack host."); + return Optional.empty(); + } + else + { + return Optional.of(RemoteHostEndPoint.getHypervisorHostEndPoint(host)); + } + } + + private Optional getDiskfullEP(DevelopersApi api, String rscName) throws ApiException { + com.linbit.linstor.api.model.StoragePool linSP = + LinstorUtil.getDiskfulStoragePool(api, rscName); + if (linSP != null) + { + Host host = _hostDao.findByName(linSP.getNodeName()); + if (host == null) + { + s_logger.error("Linstor: Host '" + linSP.getNodeName() + "' not found."); + return Optional.empty(); + } + else + { + return Optional.of(RemoteHostEndPoint.getHypervisorHostEndPoint(host)); + } + } + return Optional.empty(); + } + + private Answer copyTemplate(DataObject srcData, DataObject dstData) { + TemplateInfo tInfo = (TemplateInfo) dstData; + final StoragePoolVO pool = _storagePoolDao.findById(dstData.getDataStore().getId()); + final DevelopersApi api = LinstorUtil.getLinstorAPI(pool.getHostAddress()); + final String rscName = LinstorUtil.RSC_PREFIX + dstData.getUuid(); + createResourceBase( + LinstorUtil.RSC_PREFIX + dstData.getUuid(), + tInfo.getSize(), + tInfo.getName(), + "", + api, + getRscGrp(pool)); + + int nMaxExecutionMinutes = NumbersUtil.parseInt( + _configDao.getValue(Config.SecStorageCmdExecutionTimeMax.key()), 30); + CopyCommand cmd = new CopyCommand( + srcData.getTO(), + dstData.getTO(), + nMaxExecutionMinutes * 60 * 1000, + 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) { + s_logger.error("copy template failed: ", exc); + throw new CloudRuntimeException(exc.getBestMessage()); + } + return answer; + } + + protected Answer copySnapshot(DataObject srcData, DataObject destData) { + String value = _configDao.getValue(Config.BackupSnapshotWait.toString()); + int _backupsnapshotwait = NumbersUtil.parseInt( + value, Integer.parseInt(Config.BackupSnapshotWait.getDefaultValue())); + + SnapshotInfo snapshotInfo = (SnapshotInfo)srcData; + Boolean snapshotFullBackup = snapshotInfo.getFullBackup(); + final StoragePoolVO pool = _storagePoolDao.findById(srcData.getDataStore().getId()); + final DevelopersApi api = LinstorUtil.getLinstorAPI(pool.getHostAddress()); + boolean fullSnapshot = true; + if (snapshotFullBackup != null) { + fullSnapshot = snapshotFullBackup; + } + Map options = new HashMap<>(); + options.put("fullSnapshot", fullSnapshot + ""); + options.put(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.key(), + String.valueOf(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value())); + options.put("volumeSize", snapshotInfo.getBaseVolume().getSize() + ""); + + try { + CopyCommand cmd = new LinstorBackupSnapshotCommand( + srcData.getTO(), + destData.getTO(), + _backupsnapshotwait, + VirtualMachineManager.ExecuteInSequence.value()); + cmd.setOptions(options); + + Optional optEP = getDiskfullEP( + api, LinstorUtil.RSC_PREFIX + snapshotInfo.getBaseVolume().getUuid()); + Answer answer; + if (optEP.isPresent()) { + answer = optEP.get().sendMessage(cmd); + } else { + answer = new Answer(cmd, false, "Unable to get matching Linstor endpoint."); + } + return answer; + } catch (Exception e) { + s_logger.debug("copy snapshot failed: ", e); + throw new CloudRuntimeException(e.toString()); + } + } @Override public void copyAsync(DataObject srcData, DataObject destData, Host destHost, AsyncCompletionCallback callback) { // as long as canCopy is false, this isn't called - s_logger.debug("Linstor: copyAsync with srcdata: " + srcData.getUuid()); + s_logger.debug("Linstor: copyAsync with host"); + copyAsync(srcData, destData, callback); } private CreateCmdResult notifyResize( @@ -794,6 +1062,23 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver s_logger.debug("Linstor: handleQualityOfServiceForVolumeMigration"); } + private Answer createAnswerAndPerstistDetails(DevelopersApi api, SnapshotInfo snapshotInfo, String rscName) + throws ApiException { + SnapshotObjectTO snapshotTO = (SnapshotObjectTO)snapshotInfo.getTO(); + com.linbit.linstor.api.model.StoragePool linStoragePool = LinstorUtil.getDiskfulStoragePool(api, rscName); + if (linStoragePool == null) { + throw new CloudRuntimeException("Linstor: Unable to find storage pool for resource " + rscName); + } + + final String path = LinstorUtil.getSnapshotPath(linStoragePool, rscName, LinstorUtil.RSC_PREFIX + snapshotInfo.getUuid()); + snapshotTO.setPath(path); + SnapshotDetailsVO details = new SnapshotDetailsVO( + snapshotInfo.getId(), snapshotInfo.getUuid(), path, false); + _snapshotDetailsDao.persist(details); + + return new CreateObjectAnswer(snapshotTO); + } + @Override public void takeSnapshot(SnapshotInfo snapshotInfo, AsyncCompletionCallback callback) { @@ -823,12 +1108,11 @@ public class LinstorPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver result.setResult(errMsg); } else { - s_logger.info(String.format("Successfully took snapshot from %s", rscName)); + s_logger.info(String.format("Successfully took snapshot %s from %s", snapshot.getName(), rscName)); - SnapshotObjectTO snapshotObjectTo = (SnapshotObjectTO)snapshotInfo.getTO(); - snapshotObjectTo.setPath(rscName + "-" + snapshotInfo.getName()); + Answer answer = createAnswerAndPerstistDetails(api, snapshotInfo, rscName); - result = new CreateCmdResult(null, new CreateObjectAnswer(snapshotObjectTo)); + result = new CreateCmdResult(null, answer); result.setResult(null); } } catch (ApiException apiExc) diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorPrimaryDatastoreProviderImpl.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorPrimaryDatastoreProviderImpl.java index de0a16986c5..563f542db37 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorPrimaryDatastoreProviderImpl.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/provider/LinstorPrimaryDatastoreProviderImpl.java @@ -27,16 +27,16 @@ import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreProvider; import org.apache.cloudstack.storage.datastore.driver.LinstorPrimaryDataStoreDriverImpl; import org.apache.cloudstack.storage.datastore.lifecycle.LinstorPrimaryDataStoreLifeCycleImpl; +import org.apache.cloudstack.storage.datastore.util.LinstorUtil; public class LinstorPrimaryDatastoreProviderImpl implements PrimaryDataStoreProvider { - private final static String PROVIDER_NAME = "Linstor"; protected PrimaryDataStoreDriver driver; protected HypervisorHostListener listener; protected DataStoreLifeCycle lifecycle; @Override public String getName() { - return PROVIDER_NAME; + return LinstorUtil.PROVIDER_NAME; } @Override diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java new file mode 100644 index 00000000000..16cc24a78d4 --- /dev/null +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorConfigurationManager.java @@ -0,0 +1,40 @@ +// 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 org.apache.cloudstack.storage.datastore.util; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + +public class LinstorConfigurationManager implements Configurable +{ + public static final ConfigKey BackupSnapshots = new ConfigKey<>(Boolean.class, "lin.backup.snapshots", "Advanced", "false", + "Backup Linstor primary storage snapshots to secondary storage (deleting ps snapshot), only works on hyperconverged setups.", true, ConfigKey.Scope.Global, null); + + public static final ConfigKey[] CONFIG_KEYS = new ConfigKey[] { BackupSnapshots }; + + @Override + public String getConfigComponentName() + { + return LinstorConfigurationManager.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() + { + return CONFIG_KEYS; + } +} diff --git a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java index f1760a003ab..cd2191a6ef1 100644 --- a/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java +++ b/plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java @@ -20,12 +20,20 @@ import com.linbit.linstor.api.ApiClient; import com.linbit.linstor.api.ApiException; import com.linbit.linstor.api.Configuration; import com.linbit.linstor.api.DevelopersApi; +import com.linbit.linstor.api.model.ApiCallRc; +import com.linbit.linstor.api.model.ApiCallRcList; +import com.linbit.linstor.api.model.Node; import com.linbit.linstor.api.model.ProviderKind; import com.linbit.linstor.api.model.ResourceGroup; +import com.linbit.linstor.api.model.ResourceWithVolumes; import com.linbit.linstor.api.model.StoragePool; +import com.linbit.linstor.api.model.Volume; + +import javax.annotation.Nonnull; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.log4j.Logger; @@ -33,6 +41,7 @@ import org.apache.log4j.Logger; public class LinstorUtil { private static final Logger s_logger = Logger.getLogger(LinstorUtil.class); + public final static String PROVIDER_NAME = "Linstor"; public static final String RSC_PREFIX = "cs-"; public static final String RSC_GROUP = "resourceGroup"; @@ -47,6 +56,86 @@ public class LinstorUtil { return new DevelopersApi(client); } + public static String getBestErrorMessage(ApiCallRcList answers) { + return answers != null && !answers.isEmpty() ? + answers.stream() + .filter(ApiCallRc::isError) + .findFirst() + .map(ApiCallRc::getMessage) + .orElse((answers.get(0)).getMessage()) : null; + } + + public static List getLinstorNodeNames(@Nonnull DevelopersApi api) throws ApiException + { + List nodes = api.nodeList( + Collections.emptyList(), + Collections.emptyList(), + null, + null + ); + + return nodes.stream().map(Node::getName).collect(Collectors.toList()); + } + + public static com.linbit.linstor.api.model.StoragePool + getDiskfulStoragePool(@Nonnull DevelopersApi api, @Nonnull String rscName) throws ApiException + { + List resources = api.viewResources( + Collections.emptyList(), + Collections.singletonList(rscName), + Collections.emptyList(), + Collections.emptyList(), + null, + null); + + String nodeName = null; + String storagePoolName = null; + for (ResourceWithVolumes rwv : resources) { + if (rwv.getVolumes().isEmpty()) { + continue; + } + Volume vol = rwv.getVolumes().get(0); + if (vol.getProviderKind() != ProviderKind.DISKLESS) { + nodeName = rwv.getNodeName(); + storagePoolName = vol.getStoragePoolName(); + break; + } + } + + if (nodeName == null) { + return null; + } + + List sps = api.viewStoragePools( + Collections.singletonList(nodeName), + Collections.singletonList(storagePoolName), + Collections.emptyList(), + null, + null + ); + return !sps.isEmpty() ? sps.get(0) : null; + } + + public static String getSnapshotPath(com.linbit.linstor.api.model.StoragePool sp, String rscName, String snapshotName) { + final String suffix = "00000"; + final String backingPool = sp.getProps().get("StorDriver/StorPoolName"); + final String path; + switch (sp.getProviderKind()) { + case LVM_THIN: + path = String.format("/dev/mapper/%s-%s_%s_%s", + backingPool.split("/")[0], rscName.replace("-", "--"), suffix, snapshotName.replace("-", "--")); + break; + case ZFS: + case ZFS_THIN: + path = String.format("zfs://%s/%s_%s@%s", backingPool.split("/")[0], rscName, suffix, snapshotName); + break; + default: + throw new CloudRuntimeException( + String.format("Linstor: storage pool type %s doesn't support snapshots.", sp.getProviderKind())); + } + return path; + } + public static long getCapacityBytes(String linstorUrl, String rscGroupName) { DevelopersApi linstorApi = getLinstorAPI(linstorUrl); try { diff --git a/plugins/storage/volume/linstor/src/main/resources/META-INF/cloudstack/storage-volume-linstor/spring-storage-volume-linstor-context.xml b/plugins/storage/volume/linstor/src/main/resources/META-INF/cloudstack/storage-volume-linstor/spring-storage-volume-linstor-context.xml index e4fcb456263..48913cdb3f2 100644 --- a/plugins/storage/volume/linstor/src/main/resources/META-INF/cloudstack/storage-volume-linstor/spring-storage-volume-linstor-context.xml +++ b/plugins/storage/volume/linstor/src/main/resources/META-INF/cloudstack/storage-volume-linstor/spring-storage-volume-linstor-context.xml @@ -29,4 +29,6 @@ + diff --git a/pom.xml b/pom.xml index 365b13a3fbf..6200b8cabe4 100644 --- a/pom.xml +++ b/pom.xml @@ -699,6 +699,11 @@ xml-apis 2.0.2 + + com.linbit.linstor.api + java-linstor + ${cs.java-linstor.version} +