Add support to RBD erasure code pools (#9808)

* Readd filename string on qemuimg create

* Remove empty object on the data pool details of storage pools with no data pool

* Only use the method createPhysicalDiskByLibVirt with RBD when the pool is of erasure code type. Also added javadoc for createPhysicalDisk method

* Change literal '/' string to File.separator

* Add support for erasure code pools

* Fix null on putAll
This commit is contained in:
Bryan Lima 2025-04-02 13:19:00 +02:00 committed by GitHub
parent 2dfe6a6333
commit cb4848bc1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 312 additions and 111 deletions

View File

@ -149,6 +149,10 @@ public class StoragePoolResponse extends BaseResponseWithAnnotations {
@Param(description = "whether this pool is managed or not") @Param(description = "whether this pool is managed or not")
private Boolean managed; private Boolean managed;
@SerializedName(ApiConstants.DETAILS)
@Param(description = "the storage pool details")
private Map<String, String> details;
public Map<String, String> getCaps() { public Map<String, String> getCaps() {
return caps; return caps;
} }
@ -407,4 +411,12 @@ public class StoragePoolResponse extends BaseResponseWithAnnotations {
public void setManaged(Boolean managed) { public void setManaged(Boolean managed) {
this.managed = managed; this.managed = managed;
} }
public Map<String, String> getDetails() {
return details;
}
public void setDetails(Map<String, String> details) {
this.details = details;
}
} }

View File

@ -43,7 +43,6 @@ import com.cloud.storage.StoragePool;
import com.cloud.storage.StoragePoolHostVO; import com.cloud.storage.StoragePoolHostVO;
import com.cloud.storage.StorageService; import com.cloud.storage.StorageService;
import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.storage.dao.StoragePoolHostDao;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
@ -60,6 +59,7 @@ import javax.inject.Inject;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
public class DefaultHostListener implements HypervisorHostListener { public class DefaultHostListener implements HypervisorHostListener {
protected Logger logger = LogManager.getLogger(getClass()); protected Logger logger = LogManager.getLogger(getClass());
@ -133,9 +133,11 @@ public class DefaultHostListener implements HypervisorHostListener {
@Override @Override
public boolean hostConnect(long hostId, long poolId) throws StorageConflictException { public boolean hostConnect(long hostId, long poolId) throws StorageConflictException {
StoragePool pool = (StoragePool) this.dataStoreMgr.getDataStore(poolId, DataStoreRole.Primary); StoragePool pool = (StoragePool) this.dataStoreMgr.getDataStore(poolId, DataStoreRole.Primary);
Pair<Map<String, String>, Boolean> nfsMountOpts = storageManager.getStoragePoolNFSMountOpts(pool, null); Map<String, String> detailsMap = storagePoolDetailsDao.listDetailsKeyPairs(poolId);
Map<String, String> nfsMountOpts = storageManager.getStoragePoolNFSMountOpts(pool, null).first();
ModifyStoragePoolCommand cmd = new ModifyStoragePoolCommand(true, pool, nfsMountOpts.first()); Optional.ofNullable(nfsMountOpts).ifPresent(detailsMap::putAll);
ModifyStoragePoolCommand cmd = new ModifyStoragePoolCommand(true, pool, detailsMap);
cmd.setWait(modifyStoragePoolCommandWait); cmd.setWait(modifyStoragePoolCommandWait);
HostVO host = hostDao.findById(hostId); HostVO host = hostDao.findById(hostId);
logger.debug("Sending modify storage pool command to agent: {} for storage pool: {} with timeout {} seconds", host, pool, cmd.getWait()); logger.debug("Sending modify storage pool command to agent: {} for storage pool: {} with timeout {} seconds", host, pool, cmd.getWait());

View File

@ -108,9 +108,7 @@ public final class LibvirtCreatePrivateTemplateFromVolumeCommandWrapper extends
} else { } else {
logger.debug("Converting RBD disk " + disk.getPath() + " into template " + command.getUniqueName()); logger.debug("Converting RBD disk " + disk.getPath() + " into template " + command.getUniqueName());
final QemuImgFile srcFile = final QemuImgFile srcFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(primary, disk.getPath()));
new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(primary.getSourceHost(), primary.getSourcePort(), primary.getAuthUserName(),
primary.getAuthSecret(), disk.getPath()));
srcFile.setFormat(PhysicalDiskFormat.RAW); srcFile.setFormat(PhysicalDiskFormat.RAW);
final QemuImgFile destFile = new QemuImgFile(tmpltPath + "/" + command.getUniqueName() + ".qcow2"); final QemuImgFile destFile = new QemuImgFile(tmpltPath + "/" + command.getUniqueName() + ".qcow2");

View File

@ -161,11 +161,7 @@ public final class LibvirtGetVolumesOnStorageCommandWrapper extends CommandWrapp
QemuImg qemu = new QemuImg(0); QemuImg qemu = new QemuImg(0);
QemuImgFile qemuFile = new QemuImgFile(disk.getPath(), disk.getFormat()); QemuImgFile qemuFile = new QemuImgFile(disk.getPath(), disk.getFormat());
if (StoragePoolType.RBD.equals(pool.getType())) { if (StoragePoolType.RBD.equals(pool.getType())) {
String rbdDestFile = KVMPhysicalDisk.RBDStringBuilder(pool.getSourceHost(), String rbdDestFile = KVMPhysicalDisk.RBDStringBuilder(pool, disk.getPath());
pool.getSourcePort(),
pool.getAuthUserName(),
pool.getAuthSecret(),
disk.getPath());
qemuFile = new QemuImgFile(rbdDestFile, disk.getFormat()); qemuFile = new QemuImgFile(rbdDestFile, disk.getFormat());
} }
return qemu.info(qemuFile, secure); return qemu.info(qemuFile, secure);

View File

@ -410,9 +410,7 @@ public class IscsiAdmStorageAdaptor implements StorageAdaptor {
KVMStoragePool srcPool = srcDisk.getPool(); KVMStoragePool srcPool = srcDisk.getPool();
if (srcPool.getType() == StoragePoolType.RBD) { if (srcPool.getType() == StoragePoolType.RBD) {
srcFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(srcPool.getSourceHost(), srcPool.getSourcePort(), srcFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(srcPool, srcDisk.getPath()), srcDisk.getFormat());
srcPool.getAuthUserName(), srcPool.getAuthSecret(),
srcDisk.getPath()),srcDisk.getFormat());
} else { } else {
srcFile = new QemuImgFile(srcDisk.getPath(), srcDisk.getFormat()); srcFile = new QemuImgFile(srcDisk.getPath(), srcDisk.getFormat());
} }

View File

@ -23,6 +23,7 @@ import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
public class KVMPhysicalDisk { public class KVMPhysicalDisk {
private String path; private String path;
@ -32,10 +33,17 @@ public class KVMPhysicalDisk {
private String vmName; private String vmName;
private boolean useAsTemplate; private boolean useAsTemplate;
public static String RBDStringBuilder(String monHost, int monPort, String authUserName, String authSecret, String image) { public static final String RBD_DEFAULT_DATA_POOL = "rbd_default_data_pool";
String rbdOpts;
rbdOpts = "rbd:" + image; public static String RBDStringBuilder(KVMStoragePool storagePool, String image) {
String monHost = storagePool.getSourceHost();
int monPort = storagePool.getSourcePort();
String authUserName = storagePool.getAuthUserName();
String authSecret = storagePool.getAuthSecret();
Map<String, String> details = storagePool.getDetails();
String dataPool = (details == null) ? null : details.get(RBD_DEFAULT_DATA_POOL);
String rbdOpts = "rbd:" + image;
rbdOpts += ":mon_host=" + composeOptionForMonHosts(monHost, monPort); rbdOpts += ":mon_host=" + composeOptionForMonHosts(monHost, monPort);
if (authUserName == null) { if (authUserName == null) {
@ -46,6 +54,10 @@ public class KVMPhysicalDisk {
rbdOpts += ":key=" + authSecret; rbdOpts += ":key=" + authSecret;
} }
if (dataPool != null) {
rbdOpts += String.format(":rbd_default_data_pool=%s", dataPool);
}
rbdOpts += ":rbd_default_format=2"; rbdOpts += ":rbd_default_format=2";
rbdOpts += ":client_mount_timeout=30"; rbdOpts += ":client_mount_timeout=30";

View File

@ -53,28 +53,6 @@ import com.cloud.vm.VirtualMachine;
public class KVMStoragePoolManager { public class KVMStoragePoolManager {
protected Logger logger = LogManager.getLogger(getClass()); protected Logger logger = LogManager.getLogger(getClass());
private class StoragePoolInformation {
String name;
String host;
int port;
String path;
String userInfo;
boolean type;
StoragePoolType poolType;
Map<String, String> details;
public StoragePoolInformation(String name, String host, int port, String path, String userInfo, StoragePoolType poolType, Map<String, String> details, boolean type) {
this.name = name;
this.host = host;
this.port = port;
this.path = path;
this.userInfo = userInfo;
this.type = type;
this.poolType = poolType;
this.details = details;
}
}
private KVMHAMonitor _haMonitor; private KVMHAMonitor _haMonitor;
private final Map<String, StoragePoolInformation> _storagePools = new ConcurrentHashMap<String, StoragePoolInformation>(); private final Map<String, StoragePoolInformation> _storagePools = new ConcurrentHashMap<String, StoragePoolInformation>();
private final Map<String, StorageAdaptor> _storageMapper = new HashMap<String, StorageAdaptor>(); private final Map<String, StorageAdaptor> _storageMapper = new HashMap<String, StorageAdaptor>();
@ -303,14 +281,33 @@ public class KVMStoragePoolManager {
} catch (Exception e) { } catch (Exception e) {
StoragePoolInformation info = _storagePools.get(uuid); StoragePoolInformation info = _storagePools.get(uuid);
if (info != null) { if (info != null) {
pool = createStoragePool(info.name, info.host, info.port, info.path, info.userInfo, info.poolType, info.details, info.type); pool = createStoragePool(info.getName(), info.getHost(), info.getPort(), info.getPath(), info.getUserInfo(), info.getPoolType(), info.getDetails(), info.isType());
} else { } else {
throw new CloudRuntimeException("Could not fetch storage pool " + uuid + " from libvirt due to " + e.getMessage()); throw new CloudRuntimeException("Could not fetch storage pool " + uuid + " from libvirt due to " + e.getMessage());
} }
} }
if (pool instanceof LibvirtStoragePool) {
addPoolDetails(uuid, (LibvirtStoragePool) pool);
}
return pool; return pool;
} }
/**
* As the class {@link LibvirtStoragePool} is constrained to the {@link org.libvirt.StoragePool} class, there is no way of saving a generic parameter such as the details, hence,
* this method was created to always make available the details of libvirt primary storages for when they are needed.
*/
private void addPoolDetails(String uuid, LibvirtStoragePool pool) {
StoragePoolInformation storagePoolInformation = _storagePools.get(uuid);
Map<String, String> details = storagePoolInformation.getDetails();
if (MapUtils.isNotEmpty(details)) {
logger.trace("Adding the details {} to the pool with UUID {}.", details, uuid);
pool.setDetails(details);
}
}
public KVMStoragePool getStoragePoolByURI(String uri) { public KVMStoragePool getStoragePoolByURI(String uri) {
URI storageUri = null; URI storageUri = null;

View File

@ -667,9 +667,7 @@ public class KVMStorageProcessor implements StorageProcessor {
} else { } else {
logger.debug("Converting RBD disk " + disk.getPath() + " into template " + templateName); logger.debug("Converting RBD disk " + disk.getPath() + " into template " + templateName);
final QemuImgFile srcFile = final QemuImgFile srcFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(primary, disk.getPath()));
new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(primary.getSourceHost(), primary.getSourcePort(), primary.getAuthUserName(),
primary.getAuthSecret(), disk.getPath()));
srcFile.setFormat(PhysicalDiskFormat.RAW); srcFile.setFormat(PhysicalDiskFormat.RAW);
final QemuImgFile destFile = new QemuImgFile(tmpltPath + "/" + templateName + ".qcow2"); final QemuImgFile destFile = new QemuImgFile(tmpltPath + "/" + templateName + ".qcow2");
@ -1022,9 +1020,7 @@ public class KVMStorageProcessor implements StorageProcessor {
logger.debug("Attempting to create " + snapDir.getAbsolutePath() + " recursively for snapshot storage"); logger.debug("Attempting to create " + snapDir.getAbsolutePath() + " recursively for snapshot storage");
FileUtils.forceMkdir(snapDir); FileUtils.forceMkdir(snapDir);
final QemuImgFile srcFile = final QemuImgFile srcFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(primaryPool, rbdSnapshot));
new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(primaryPool.getSourceHost(), primaryPool.getSourcePort(), primaryPool.getAuthUserName(),
primaryPool.getAuthSecret(), rbdSnapshot));
srcFile.setFormat(snapshotDisk.getFormat()); srcFile.setFormat(snapshotDisk.getFormat());
final QemuImgFile destFile = new QemuImgFile(snapshotFile); final QemuImgFile destFile = new QemuImgFile(snapshotFile);

View File

@ -960,17 +960,55 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
} }
} }
/**
* Creates a physical disk depending on the {@link StoragePoolType}:
* <ul>
* <li>
* <b>{@link StoragePoolType#RBD}</b>
* <ul>
* <li>
* If it is an erasure code pool, utilizes QemuImg to create the physical disk through the method
* {@link LibvirtStorageAdaptor#createPhysicalDiskByQemuImg(String, KVMStoragePool, PhysicalDiskFormat, Storage.ProvisioningType, long, byte[])}
* </li>
* <li>
* Otherwise, utilize Libvirt to create the physical disk through the method
* {@link LibvirtStorageAdaptor#createPhysicalDiskByLibVirt(String, KVMStoragePool, PhysicalDiskFormat, Storage.ProvisioningType, long)}
* </li>
* </ul>
* </li>
* <li>
* {@link StoragePoolType#NetworkFilesystem} and {@link StoragePoolType#Filesystem}
* <ul>
* <li>
* If the format is {@link PhysicalDiskFormat#QCOW2} or {@link PhysicalDiskFormat#RAW}, utilizes QemuImg to create the physical disk through the method
* {@link LibvirtStorageAdaptor#createPhysicalDiskByQemuImg(String, KVMStoragePool, PhysicalDiskFormat, Storage.ProvisioningType, long, byte[])}
* </li>
* <li>
* If the format is {@link PhysicalDiskFormat#DIR} or {@link PhysicalDiskFormat#TAR}, utilize Libvirt to create the physical disk through the method
* {@link LibvirtStorageAdaptor#createPhysicalDiskByLibVirt(String, KVMStoragePool, PhysicalDiskFormat, Storage.ProvisioningType, long)}
* </li>
* </ul>
* </li>
* <li>
* For the rest of the {@link StoragePoolType} types, utilizes the Libvirt method
* {@link LibvirtStorageAdaptor#createPhysicalDiskByLibVirt(String, KVMStoragePool, PhysicalDiskFormat, Storage.ProvisioningType, long)}
* </li>
* </ul>
*/
@Override @Override
public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool, public KVMPhysicalDisk createPhysicalDisk(String name, KVMStoragePool pool,
PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase) { PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase) {
logger.info("Attempting to create volume " + name + " (" + pool.getType().toString() + ") in pool " logger.info("Attempting to create volume {} ({}) in pool {} with size {}", name, pool.getType().toString(), pool.getUuid(), toHumanReadableSize(size));
+ pool.getUuid() + " with size " + toHumanReadableSize(size));
StoragePoolType poolType = pool.getType(); StoragePoolType poolType = pool.getType();
if (poolType.equals(StoragePoolType.RBD)) { if (StoragePoolType.RBD.equals(poolType)) {
return createPhysicalDiskByLibVirt(name, pool, PhysicalDiskFormat.RAW, provisioningType, size); Map<String, String> details = pool.getDetails();
} else if (poolType.equals(StoragePoolType.NetworkFilesystem) || poolType.equals(StoragePoolType.Filesystem)) { String dataPool = (details == null) ? null : details.get(KVMPhysicalDisk.RBD_DEFAULT_DATA_POOL);
return (dataPool == null) ? createPhysicalDiskByLibVirt(name, pool, PhysicalDiskFormat.RAW, provisioningType, size) :
createPhysicalDiskByQemuImg(name, pool, PhysicalDiskFormat.RAW, provisioningType, size, passphrase);
} else if (StoragePoolType.NetworkFilesystem.equals(poolType) || StoragePoolType.Filesystem.equals(poolType)) {
switch (format) { switch (format) {
case QCOW2: case QCOW2:
case RAW: case RAW:
@ -1018,18 +1056,25 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
} }
private KVMPhysicalDisk createPhysicalDiskByQemuImg(String name, KVMStoragePool pool, private KVMPhysicalDisk createPhysicalDiskByQemuImg(String name, KVMStoragePool pool, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size,
PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, byte[] passphrase) { byte[] passphrase) {
String volPath = pool.getLocalPath() + "/" + name; String volPath;
String volName = name; String volName = name;
long virtualSize = 0; long virtualSize = 0;
long actualSize = 0; long actualSize = 0;
QemuObject.EncryptFormat encryptFormat = null; QemuObject.EncryptFormat encryptFormat = null;
List<QemuObject> passphraseObjects = new ArrayList<>(); List<QemuObject> passphraseObjects = new ArrayList<>();
final int timeout = 0; final int timeout = 0;
QemuImgFile destFile;
if (StoragePoolType.RBD.equals(pool.getType())) {
volPath = pool.getSourceDir() + File.separator + name;
destFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(pool, volPath));
} else {
volPath = pool.getLocalPath() + File.separator + name;
destFile = new QemuImgFile(volPath);
}
QemuImgFile destFile = new QemuImgFile(volPath);
destFile.setFormat(format); destFile.setFormat(format);
destFile.setSize(size); destFile.setSize(size);
Map<String, String> options = new HashMap<String, String>(); Map<String, String> options = new HashMap<String, String>();
@ -1312,11 +1357,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
QemuImgFile srcFile; QemuImgFile srcFile;
QemuImgFile destFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(destPool.getSourceHost(), QemuImgFile destFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(destPool, disk.getPath()));
destPool.getSourcePort(),
destPool.getAuthUserName(),
destPool.getAuthSecret(),
disk.getPath()));
destFile.setFormat(format); destFile.setFormat(format);
if (srcPool.getType() != StoragePoolType.RBD) { if (srcPool.getType() != StoragePoolType.RBD) {
@ -1591,11 +1632,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
try { try {
srcFile = new QemuImgFile(sourcePath, sourceFormat); srcFile = new QemuImgFile(sourcePath, sourceFormat);
String rbdDestPath = destPool.getSourceDir() + "/" + name; String rbdDestPath = destPool.getSourceDir() + "/" + name;
String rbdDestFile = KVMPhysicalDisk.RBDStringBuilder(destPool.getSourceHost(), String rbdDestFile = KVMPhysicalDisk.RBDStringBuilder(destPool, rbdDestPath);
destPool.getSourcePort(),
destPool.getAuthUserName(),
destPool.getAuthSecret(),
rbdDestPath);
destFile = new QemuImgFile(rbdDestFile, destFormat); destFile = new QemuImgFile(rbdDestFile, destFormat);
logger.debug("Starting copy from source image " + srcFile.getFileName() + " to RBD image " + rbdDestPath); logger.debug("Starting copy from source image " + srcFile.getFileName() + " to RBD image " + rbdDestPath);
@ -1638,9 +1675,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
We let Qemu-Img do the work here. Although we could work with librbd and have that do the cloning We let Qemu-Img do the work here. Although we could work with librbd and have that do the cloning
it doesn't benefit us. It's better to keep the current code in place which works it doesn't benefit us. It's better to keep the current code in place which works
*/ */
srcFile = srcFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(srcPool, sourcePath));
new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(srcPool.getSourceHost(), srcPool.getSourcePort(), srcPool.getAuthUserName(), srcPool.getAuthSecret(),
sourcePath));
srcFile.setFormat(sourceFormat); srcFile.setFormat(sourceFormat);
destFile = new QemuImgFile(destPath); destFile = new QemuImgFile(destPath);
destFile.setFormat(destFormat); destFile.setFormat(destFormat);

View File

@ -56,8 +56,8 @@ public class LibvirtStoragePool implements KVMStoragePool {
protected String authSecret; protected String authSecret;
protected String sourceHost; protected String sourceHost;
protected int sourcePort; protected int sourcePort;
protected String sourceDir; protected String sourceDir;
protected Map<String, String> details;
public LibvirtStoragePool(String uuid, String name, StoragePoolType type, StorageAdaptor adaptor, StoragePool pool) { public LibvirtStoragePool(String uuid, String name, StoragePoolType type, StorageAdaptor adaptor, StoragePool pool) {
this.uuid = uuid; this.uuid = uuid;
@ -311,7 +311,11 @@ public class LibvirtStoragePool implements KVMStoragePool {
@Override @Override
public Map<String, String> getDetails() { public Map<String, String> getDetails() {
return null; return this.details;
}
public void setDetails(Map<String, String> details) {
this.details = details;
} }
@Override @Override

View File

@ -0,0 +1,75 @@
// 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.storage.Storage;
import java.util.Map;
class StoragePoolInformation {
private String name;
private String host;
private int port;
private String path;
private String userInfo;
private boolean type;
private Storage.StoragePoolType poolType;
private Map<String, String> details;
public StoragePoolInformation(String name, String host, int port, String path, String userInfo, Storage.StoragePoolType poolType, Map<String, String> details, boolean type) {
this.name = name;
this.host = host;
this.port = port;
this.path = path;
this.userInfo = userInfo;
this.type = type;
this.poolType = poolType;
this.details = details;
}
public String getName() {
return name;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public String getPath() {
return path;
}
public String getUserInfo() {
return userInfo;
}
public boolean isType() {
return type;
}
public Storage.StoragePoolType getPoolType() {
return poolType;
}
public Map<String, String> getDetails() {
return details;
}
}

View File

@ -17,43 +17,73 @@
package com.cloud.hypervisor.kvm.storage; package com.cloud.hypervisor.kvm.storage;
import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import junit.framework.TestCase;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class KVMPhysicalDiskTest extends TestCase { public class KVMPhysicalDiskTest {
@Mock
KVMStoragePool kvmStoragePoolMock;
private final String authUserName = "admin";
private final String authSecret = "supersecret";
@Test @Test
public void testRBDStringBuilder() { public void testRBDStringBuilder() {
assertEquals(KVMPhysicalDisk.RBDStringBuilder("ceph-monitor", 8000, "admin", "supersecret", "volume1"), String monHosts = "ceph-monitor";
"rbd:volume1:mon_host=ceph-monitor\\:8000:auth_supported=cephx:id=admin:key=supersecret:rbd_default_format=2:client_mount_timeout=30"); int monPort = 8000;
Mockito.doReturn(monHosts).when(kvmStoragePoolMock).getSourceHost();
Mockito.doReturn(monPort).when(kvmStoragePoolMock).getSourcePort();
Mockito.doReturn(authUserName).when(kvmStoragePoolMock).getAuthUserName();
Mockito.doReturn(authSecret).when(kvmStoragePoolMock).getAuthSecret();
String expected = "rbd:volume1:mon_host=ceph-monitor\\:8000:auth_supported=cephx:id=admin:key=supersecret:rbd_default_format=2:client_mount_timeout=30";
String result = KVMPhysicalDisk.RBDStringBuilder(kvmStoragePoolMock, "volume1");
Assert.assertEquals(expected, result);
} }
@Test @Test
public void testRBDStringBuilder2() { public void testRBDStringBuilder2() {
String monHosts = "ceph-monitor1,ceph-monitor2,ceph-monitor3"; String monHosts = "ceph-monitor1,ceph-monitor2,ceph-monitor3";
int monPort = 3300; int monPort = 3300;
Mockito.doReturn(monHosts).when(kvmStoragePoolMock).getSourceHost();
Mockito.doReturn(monPort).when(kvmStoragePoolMock).getSourcePort();
Mockito.doReturn(authUserName).when(kvmStoragePoolMock).getAuthUserName();
Mockito.doReturn(authSecret).when(kvmStoragePoolMock).getAuthSecret();
String expected = "rbd:volume1:" + String expected = "rbd:volume1:" +
"mon_host=ceph-monitor1\\:3300\\;ceph-monitor2\\:3300\\;ceph-monitor3\\:3300:" + "mon_host=ceph-monitor1\\:3300\\;ceph-monitor2\\:3300\\;ceph-monitor3\\:3300:" +
"auth_supported=cephx:id=admin:key=supersecret:rbd_default_format=2:client_mount_timeout=30"; "auth_supported=cephx:id=admin:key=supersecret:rbd_default_format=2:client_mount_timeout=30";
String actualResult = KVMPhysicalDisk.RBDStringBuilder(monHosts, monPort, "admin", "supersecret", "volume1"); String actualResult = KVMPhysicalDisk.RBDStringBuilder(kvmStoragePoolMock, "volume1");
assertEquals(expected, actualResult);
Assert.assertEquals(expected, actualResult);
} }
@Test @Test
public void testRBDStringBuilder3() { public void testRBDStringBuilder3() {
String monHosts = "[fc00:1234::1],[fc00:1234::2],[fc00:1234::3]"; String monHosts = "[fc00:1234::1],[fc00:1234::2],[fc00:1234::3]";
int monPort = 3300; int monPort = 3300;
Mockito.doReturn(monHosts).when(kvmStoragePoolMock).getSourceHost();
Mockito.doReturn(monPort).when(kvmStoragePoolMock).getSourcePort();
Mockito.doReturn(authUserName).when(kvmStoragePoolMock).getAuthUserName();
Mockito.doReturn(authSecret).when(kvmStoragePoolMock).getAuthSecret();
String expected = "rbd:volume1:" + String expected = "rbd:volume1:" +
"mon_host=[fc00\\:1234\\:\\:1]\\:3300\\;[fc00\\:1234\\:\\:2]\\:3300\\;[fc00\\:1234\\:\\:3]\\:3300:" + "mon_host=[fc00\\:1234\\:\\:1]\\:3300\\;[fc00\\:1234\\:\\:2]\\:3300\\;[fc00\\:1234\\:\\:3]\\:3300:" +
"auth_supported=cephx:id=admin:key=supersecret:rbd_default_format=2:client_mount_timeout=30"; "auth_supported=cephx:id=admin:key=supersecret:rbd_default_format=2:client_mount_timeout=30";
String actualResult = KVMPhysicalDisk.RBDStringBuilder(monHosts, monPort, "admin", "supersecret", "volume1"); String actualResult = KVMPhysicalDisk.RBDStringBuilder(kvmStoragePoolMock, "volume1");
assertEquals(expected, actualResult);
Assert.assertEquals(expected, actualResult);
} }
@Test @Test
@ -64,18 +94,18 @@ public class KVMPhysicalDiskTest extends TestCase {
LibvirtStoragePool pool = Mockito.mock(LibvirtStoragePool.class); LibvirtStoragePool pool = Mockito.mock(LibvirtStoragePool.class);
KVMPhysicalDisk disk = new KVMPhysicalDisk(path, name, pool); KVMPhysicalDisk disk = new KVMPhysicalDisk(path, name, pool);
assertEquals(disk.getName(), name); Assert.assertEquals(disk.getName(), name);
assertEquals(disk.getPath(), path); Assert.assertEquals(disk.getPath(), path);
assertEquals(disk.getPool(), pool); Assert.assertEquals(disk.getPool(), pool);
assertEquals(disk.getSize(), 0); Assert.assertEquals(disk.getSize(), 0);
assertEquals(disk.getVirtualSize(), 0); Assert.assertEquals(disk.getVirtualSize(), 0);
disk.setSize(1024); disk.setSize(1024);
disk.setVirtualSize(2048); disk.setVirtualSize(2048);
assertEquals(disk.getSize(), 1024); Assert.assertEquals(disk.getSize(), 1024);
assertEquals(disk.getVirtualSize(), 2048); Assert.assertEquals(disk.getVirtualSize(), 2048);
disk.setFormat(PhysicalDiskFormat.RAW); disk.setFormat(PhysicalDiskFormat.RAW);
assertEquals(disk.getFormat(), PhysicalDiskFormat.RAW); Assert.assertEquals(disk.getFormat(), PhysicalDiskFormat.RAW);
} }
} }

View File

@ -114,11 +114,7 @@ public final class StorPoolDownloadVolumeCommandWrapper extends CommandWrapper<S
if (isRBDPool) { if (isRBDPool) {
KVMStoragePool srcPool = srcDisk.getPool(); KVMStoragePool srcPool = srcDisk.getPool();
String rbdDestPath = srcPool.getSourceDir() + "/" + srcDisk.getName(); String rbdDestPath = srcPool.getSourceDir() + "/" + srcDisk.getName();
srcPath = KVMPhysicalDisk.RBDStringBuilder(srcPool.getSourceHost(), srcPath = KVMPhysicalDisk.RBDStringBuilder(srcPool, rbdDestPath);
srcPool.getSourcePort(),
srcPool.getAuthUserName(),
srcPool.getAuthSecret(),
rbdDestPath);
} else { } else {
srcPath = srcDisk.getPath(); srcPath = srcDisk.getPath();
} }

View File

@ -165,6 +165,7 @@ import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils; import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -3158,28 +3159,41 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
List<StoragePoolResponse> poolResponses = ViewResponseHelper.createStoragePoolResponse(getCustomStats, storagePools.first().toArray(new StoragePoolJoinVO[storagePools.first().size()])); List<StoragePoolResponse> poolResponses = ViewResponseHelper.createStoragePoolResponse(getCustomStats, storagePools.first().toArray(new StoragePoolJoinVO[storagePools.first().size()]));
Map<String, Long> poolUuidToIdMap = storagePools.first().stream().collect(Collectors.toMap(StoragePoolJoinVO::getUuid, StoragePoolJoinVO::getId, (a, b) -> a)); Map<String, Long> poolUuidToIdMap = storagePools.first().stream().collect(Collectors.toMap(StoragePoolJoinVO::getUuid, StoragePoolJoinVO::getId, (a, b) -> a));
for (StoragePoolResponse poolResponse : poolResponses) { for (StoragePoolResponse poolResponse : poolResponses) {
Long poolId = poolUuidToIdMap.get(poolResponse.getId());
DataStore store = dataStoreManager.getPrimaryDataStore(poolResponse.getId()); DataStore store = dataStoreManager.getPrimaryDataStore(poolResponse.getId());
if (store != null) { if (store != null) {
DataStoreDriver driver = store.getDriver(); addPoolDetailsAndCapabilities(poolResponse, store, poolId);
if (driver != null && driver.getCapabilities() != null) {
Map<String, String> caps = driver.getCapabilities();
if (Storage.StoragePoolType.NetworkFilesystem.toString().equals(poolResponse.getType()) &&
HypervisorType.VMware.toString().equals(poolResponse.getHypervisor())) {
StoragePoolDetailVO detail = _storagePoolDetailsDao.findDetail(poolUuidToIdMap.get(poolResponse.getId()), Storage.Capability.HARDWARE_ACCELERATION.toString());
if (detail != null) {
caps.put(Storage.Capability.HARDWARE_ACCELERATION.toString(), detail.getValue());
}
}
poolResponse.setCaps(caps);
}
} }
setPoolResponseNFSMountOptions(poolResponse, poolUuidToIdMap.get(poolResponse.getId()));
setPoolResponseNFSMountOptions(poolResponse, poolId);
} }
response.setResponses(poolResponses, storagePools.second()); response.setResponses(poolResponses, storagePools.second());
return response; return response;
} }
private void addPoolDetailsAndCapabilities(StoragePoolResponse poolResponse, DataStore store, Long poolId) {
Map<String, String> details = _storagePoolDetailsDao.listDetailsKeyPairs(store.getId(), true);
poolResponse.setDetails(details);
DataStoreDriver driver = store.getDriver();
if (ObjectUtils.anyNull(driver, driver.getCapabilities())) {
return;
}
Map<String, String> caps = driver.getCapabilities();
if (Storage.StoragePoolType.NetworkFilesystem.toString().equals(poolResponse.getType()) && HypervisorType.VMware.toString().equals(poolResponse.getHypervisor())) {
StoragePoolDetailVO detail = _storagePoolDetailsDao.findDetail(poolId, Storage.Capability.HARDWARE_ACCELERATION.toString());
if (detail != null) {
caps.put(Storage.Capability.HARDWARE_ACCELERATION.toString(), detail.getValue());
}
}
poolResponse.setCaps(caps);
}
private Pair<List<StoragePoolJoinVO>, Integer> searchForStoragePoolsInternal(ListStoragePoolsCmd cmd) { private Pair<List<StoragePoolJoinVO>, Integer> searchForStoragePoolsInternal(ListStoragePoolsCmd cmd) {
ScopeType scopeType = ScopeType.validateAndGetScopeType(cmd.getScope()); ScopeType scopeType = ScopeType.validateAndGetScopeType(cmd.getScope());
StoragePoolStatus status = StoragePoolStatus.validateAndGetStatus(cmd.getStatus()); StoragePoolStatus status = StoragePoolStatus.validateAndGetStatus(cmd.getStatus());

View File

@ -665,6 +665,8 @@
"label.dark.mode": "Dark mode", "label.dark.mode": "Dark mode",
"label.dashboard": "Dashboard", "label.dashboard": "Dashboard",
"label.data.disk": "Data disk", "label.data.disk": "Data disk",
"label.data.pool": "Data pool",
"label.data.pool.description": "Data pool is required when using a Ceph pool with erasure code",
"label.data.disk.offering": "Data disk offering", "label.data.disk.offering": "Data disk offering",
"label.date": "Date", "label.date": "Date",
"label.datetime.filter.period": "From <b>{startDate}</b> to <b>{endDate}</b>", "label.datetime.filter.period": "From <b>{startDate}</b> to <b>{endDate}</b>",

View File

@ -463,6 +463,8 @@
"label.dashboard": "Dashboard", "label.dashboard": "Dashboard",
"label.data.disk": "Disco de dados", "label.data.disk": "Disco de dados",
"label.data.disk.offering": "Oferta de disco adicional", "label.data.disk.offering": "Oferta de disco adicional",
"label.data.pool": "Data pool",
"label.data.pool.description": "\u00c9 necess\u00e1rio informar um data pool ao utilizar um Ceph pool com erasure code",
"label.date": "Data", "label.date": "Data",
"label.day": "Dia", "label.day": "Dia",
"label.day.of.month": "Dia do m\u00eas", "label.day.of.month": "Dia do m\u00eas",

View File

@ -151,6 +151,13 @@
<div>{{ $toLocaleDate(dataResource[item]) }}</div> <div>{{ $toLocaleDate(dataResource[item]) }}</div>
</div> </div>
</a-list-item> </a-list-item>
<a-list-item v-else-if="item === 'details' && $route.meta.name === 'storagepool' && dataResource[item].rbd_default_data_pool">
<div>
<strong>{{ $t('label.data.pool') }}</strong>
<br/>
<div>{{ dataResource[item].rbd_default_data_pool }}</div>
</div>
</a-list-item>
</template> </template>
<HostInfo :resource="dataResource" v-if="$route.meta.name === 'host' && 'listHosts' in $store.getters.apis" /> <HostInfo :resource="dataResource" v-if="$route.meta.name === 'host' && 'listHosts' in $store.getters.apis" />
<DedicateData :resource="dataResource" v-if="dedicatedSectionActive" /> <DedicateData :resource="dataResource" v-if="dedicatedSectionActive" />
@ -207,7 +214,7 @@ export default {
}, },
computed: { computed: {
customDisplayItems () { customDisplayItems () {
var items = ['ip4routes', 'ip6routes', 'privatemtu', 'publicmtu', 'provider'] var items = ['ip4routes', 'ip6routes', 'privatemtu', 'publicmtu', 'provider', 'details']
if (this.$route.meta.name === 'webhookdeliveries') { if (this.$route.meta.name === 'webhookdeliveries') {
items.push('startdate') items.push('startdate')
items.push('enddate') items.push('enddate')

View File

@ -35,7 +35,7 @@ export default {
fields.push('zonename') fields.push('zonename')
return fields return fields
}, },
details: ['name', 'id', 'ipaddress', 'type', 'nfsmountopts', 'scope', 'tags', 'path', 'provider', 'hypervisor', 'overprovisionfactor', 'disksizetotal', 'disksizeallocated', 'disksizeused', 'capacityiops', 'usediops', 'clustername', 'podname', 'zonename', 'created'], details: ['name', 'id', 'ipaddress', 'type', 'details', 'nfsmountopts', 'scope', 'tags', 'path', 'provider', 'hypervisor', 'overprovisionfactor', 'disksizetotal', 'disksizeallocated', 'disksizeused', 'capacityiops', 'usediops', 'clustername', 'podname', 'zonename', 'created'],
related: [{ related: [{
name: 'volume', name: 'volume',
title: 'label.volumes', title: 'label.volumes',

View File

@ -370,6 +370,12 @@
<a-form-item name="radospool" ref="radospool" :label="$t('label.rados.pool')"> <a-form-item name="radospool" ref="radospool" :label="$t('label.rados.pool')">
<a-input v-model:value="form.radospool" :placeholder="$t('label.rados.pool')"/> <a-input v-model:value="form.radospool" :placeholder="$t('label.rados.pool')"/>
</a-form-item> </a-form-item>
<a-form-item name="datapool" ref="datapool">
<template #label>
<tooltip-label :title="$t('label.data.pool')" :tooltip="$t('label.data.pool.description')"/>
</template>
<a-input v-model:value="form.datapool" :placeholder="$t('label.data.pool')"/>
</a-form-item>
<a-form-item name="radosuser" ref="radosuser" :label="$t('label.rados.user')"> <a-form-item name="radosuser" ref="radosuser" :label="$t('label.rados.user')">
<a-input v-model:value="form.radosuser" :placeholder="$t('label.rados.user')" /> <a-input v-model:value="form.radosuser" :placeholder="$t('label.rados.user')" />
</a-form-item> </a-form-item>
@ -499,6 +505,10 @@ export default {
powerflexGatewayUsername: [{ required: true, message: this.$t('label.required') }], powerflexGatewayUsername: [{ required: true, message: this.$t('label.required') }],
powerflexGatewayPassword: [{ required: true, message: this.$t('label.required') }], powerflexGatewayPassword: [{ required: true, message: this.$t('label.required') }],
powerflexStoragePool: [{ required: true, message: this.$t('label.required') }], powerflexStoragePool: [{ required: true, message: this.$t('label.required') }],
radosmonitor: [{ required: true, message: this.$t('label.required') }],
radospool: [{ required: true, message: this.$t('label.required') }],
radosuser: [{ required: true, message: this.$t('label.required') }],
radossecret: [{ required: true, message: this.$t('label.required') }],
username: [{ required: true, message: this.$t('label.required') }], username: [{ required: true, message: this.$t('label.required') }],
password: [{ required: true, message: this.$t('label.required') }], password: [{ required: true, message: this.$t('label.required') }],
primeraURL: [{ required: true, message: this.$t('label.url') }], primeraURL: [{ required: true, message: this.$t('label.url') }],
@ -845,6 +855,9 @@ export default {
url = this.clvmURL(vg) url = this.clvmURL(vg)
} else if (values.protocol === 'RBD') { } else if (values.protocol === 'RBD') {
url = this.rbdURL(values.radosmonitor, values.radospool, values.radosuser, values.radossecret) url = this.rbdURL(values.radosmonitor, values.radospool, values.radosuser, values.radossecret)
if (values.datapool) {
params['details[0].rbd_default_data_pool'] = values.datapool
}
} else if (values.protocol === 'vmfs') { } else if (values.protocol === 'vmfs') {
path = values.vCenterDataCenter path = values.vCenterDataCenter
if (path.substring(0, 1) !== '/') { if (path.substring(0, 1) !== '/') {

View File

@ -469,7 +469,7 @@ export default {
title: 'label.rados.monitor', title: 'label.rados.monitor',
key: 'primaryStorageRADOSMonitor', key: 'primaryStorageRADOSMonitor',
placeHolder: 'message.error.rados.monitor', placeHolder: 'message.error.rados.monitor',
required: false, required: true,
display: { display: {
primaryStorageProtocol: ['rbd'] primaryStorageProtocol: ['rbd']
} }
@ -478,6 +478,14 @@ export default {
title: 'label.rados.pool', title: 'label.rados.pool',
key: 'primaryStorageRADOSPool', key: 'primaryStorageRADOSPool',
placeHolder: 'message.error.rados.pool', placeHolder: 'message.error.rados.pool',
required: true,
display: {
primaryStorageProtocol: ['rbd']
}
},
{
title: 'label.data.pool',
key: 'primaryStorageDataPool',
required: false, required: false,
display: { display: {
primaryStorageProtocol: ['rbd'] primaryStorageProtocol: ['rbd']
@ -487,7 +495,7 @@ export default {
title: 'label.rados.user', title: 'label.rados.user',
key: 'primaryStorageRADOSUser', key: 'primaryStorageRADOSUser',
placeHolder: 'message.error.rados.user', placeHolder: 'message.error.rados.user',
required: false, required: true,
display: { display: {
primaryStorageProtocol: ['rbd'] primaryStorageProtocol: ['rbd']
} }
@ -496,7 +504,7 @@ export default {
title: 'label.rados.secret', title: 'label.rados.secret',
key: 'primaryStorageRADOSSecret', key: 'primaryStorageRADOSSecret',
placeHolder: 'message.error.rados.secret', placeHolder: 'message.error.rados.secret',
required: false, required: true,
display: { display: {
primaryStorageProtocol: ['rbd'] primaryStorageProtocol: ['rbd']
} }

View File

@ -1486,6 +1486,10 @@ export default {
const rbdpool = this.prefillContent?.primaryStorageRADOSPool || '' const rbdpool = this.prefillContent?.primaryStorageRADOSPool || ''
const rbdid = this.prefillContent?.primaryStorageRADOSUser || '' const rbdid = this.prefillContent?.primaryStorageRADOSUser || ''
const rbdsecret = this.prefillContent?.primaryStorageRADOSSecret || '' const rbdsecret = this.prefillContent?.primaryStorageRADOSSecret || ''
if (this.prefillContent?.primaryStorageDataPool) {
params['details[0].rbd_default_data_pool'] = this.prefillContent.primaryStorageDataPool
}
url = this.rbdURL(rbdmonitor, rbdpool, rbdid, rbdsecret) url = this.rbdURL(rbdmonitor, rbdpool, rbdid, rbdsecret)
} else if (protocol === 'Linstor') { } else if (protocol === 'Linstor') {
url = this.linstorURL(server) url = this.linstorURL(server)