Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
26e7c3f
kvm: let libvirt remove RBD snapshots on volume delete (#13763)
wido Sep 8, 2026
b4e123d
kvm: fix RBD exclusive-lock leak that breaks revertSnapshot on Ceph (…
calvix Sep 8, 2026
f5e891f
kvm: fix restore-and-attach of a backed up volume (#14007)
abh1sar Sep 9, 2026
83ae9d6
fix: use auth_client_required instead of removed auth_supported for R…
waterWang Sep 10, 2026
5e0ee62
kvm: apply rbd_default_data_pool when creating volumes from templates…
bhouse-nexthop Sep 14, 2026
1453155
Nas backup: Fix mount/unmount error handling and timeout in LibvirtRe…
abh1sar Sep 15, 2026
9847665
backup: remove the powered-off precondition for all backup providers …
weizhouapache Sep 15, 2026
af2ca91
storage: KVM - enable RBD/Ceph volume encryption support (#13556)
calvix Sep 16, 2026
5b24e9d
Fix importVM for use with dummy template (#14195)
abh1sar Sep 19, 2026
22427c6
importVM improvements: RBD support, volume format, and cluster select…
abh1sar Sep 19, 2026
3d0bd23
smoke test: support ceph storage pools
weizhouapache Oct 29, 2025
1cb609c
test: fix test failures
weizhouapache Oct 30, 2025
ac9cac6
test: add volume migration which has been fixed
weizhouapache Dec 9, 2025
1f0fe20
test: support Ceph/RBD primary storage in NAS backup smoke test
weizhouapache Sep 8, 2026
e0e4af5
test: fix smoke test failures on xen/vmware
weizhouapache Jul 16, 2026
758bfd5
test: sleep 10 seconds before attaching volume in test_events_resourc…
weizhouapache Jul 17, 2026
9d83dcd
test: add more time.sleep(10)
weizhouapache Jul 27, 2026
167b9c7
test: add more time.sleep(10) in test_vm_strict_host_tags.py
weizhouapache Jul 28, 2026
c655d4c
test: fix test_usage.py
weizhouapache Jul 28, 2026
8f35bd4
test: move time.sleep(10) in test_host_maintenance.py
weizhouapache Jul 30, 2026
31456a7
test: retry domain deletion in tearDown of test_deploy_vms_in_paralle…
weizhouapache Aug 27, 2026
2ae2bb8
test: fix tests failures caused by others in drs and host maintenance
weizhouapache Aug 28, 2026
dd02c8b
test: fix test_10_list_volumes for zone-wide storage pools
weizhouapache Oct 5, 2026
11832e7
test_host_tags: fix cleanup to handle None host tags
weizhouapache Oct 5, 2026
38f1548
package: Add missing openssl and decompression utilities to KVM agent…
weizhouapache Oct 5, 2026
21bc6b0
kvm: support Host HA on Ceph RBD primary storage
weizhouapache Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions PendingReleaseNotes
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,12 @@ example.ver.1 > example.ver.2:
which can now be attached to Instances. This is to prevent the Secondary
Storage to grow to enormous sizes as Linux Distributions keep growing in
size while a stripped down Linux should fit on a 2.88MB floppy.

4.23.0.0 > 24.0.0:
* KVM/Ceph: RBD volumes can now be encrypted at rest using native librbd
LUKS2 (<encryption format='luks2' engine='librbd'>), for both data disks
and root disks. Encryption is transparent to the guest and reuses the
existing CloudStack volume-encryption passphrase handling, so no
additional key store is required. Note: attaching an encrypted RBD volume
to a running Instance requires libvirt >= 10.1.0; booting an Instance from
an encrypted RBD root disk works on older libvirt.
1 change: 1 addition & 0 deletions api/src/main/java/com/cloud/host/Host.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public static String[] toStrings(Host.Type... types) {

String HOST_UEFI_ENABLE = "host.uefi.enable";
String HOST_VOLUME_ENCRYPTION = "host.volume.encryption";
String HOST_RBD_VOLUME_ENCRYPTION = "host.volume.encryption.rbd";
String HOST_INSTANCE_CONVERSION = "host.instance.conversion";
String HOST_VDDK_SUPPORT = "host.vddk.support";
String HOST_VDDK_LIB_DIR = "vddk.lib.dir";
Expand Down
2 changes: 1 addition & 1 deletion api/src/main/java/com/cloud/storage/Storage.java
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public static enum StoragePoolType {
LVM(false, false, EncryptionSupport.Unsupported), // XenServer local LVM SR
CLVM(true, false, EncryptionSupport.Unsupported),
CLVM_NG(true, false, EncryptionSupport.Hypervisor),
RBD(true, true, EncryptionSupport.Unsupported), // http://libvirt.org/storage.html#StorageBackendRBD
RBD(true, true, EncryptionSupport.Hypervisor), // http://libvirt.org/storage.html#StorageBackendRBD ; encrypted natively by librbd (LUKS2, engine='librbd')
SharedMountPoint(true, true, EncryptionSupport.Hypervisor),
VMFS(true, true, EncryptionSupport.Unsupported), // VMware VMFS storage
PreSetup(true, true, EncryptionSupport.Unsupported), // for XenServer, Storage Pool is set up by customers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd {

@Parameter(name = ApiConstants.DISK_PATH,
type = CommandType.STRING,
description = "path of the disk image")
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")
private String diskPath;

@Parameter(name = ApiConstants.IMPORT_SOURCE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public class ImportVolumeCmd extends BaseAsyncCmd {
@Parameter(name = ApiConstants.PATH,
type = BaseCmd.CommandType.STRING,
required = true,
description = "the path of the volume")
description = "the path of the volume. It is the file name on file based storage pools (NFS, Local, SharedMountPoint), and the image name on RBD storage pools")
private String path;

@Parameter(name = ApiConstants.NAME,
Expand Down
2 changes: 1 addition & 1 deletion debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Description: CloudStack server library

Package: cloudstack-agent
Architecture: all
Depends: ${python:Depends}, ${python3:Depends}, openjdk-17-jre-headless | java17-runtime-headless | java17-runtime | zulu-17, cloudstack-common (= ${source:Version}), lsb-base (>= 9), openssh-client, qemu-kvm (>= 2.5) | qemu-system-x86 (>= 5.2), libvirt-bin (>= 1.3) | libvirt-daemon-system (>= 3.0), iproute2, ebtables, vlan, ipset, python3-libvirt, ethtool, iptables, cryptsetup, rng-tools, rsync, ovmf, swtpm, lsb-release, ufw, apparmor, cpu-checker, libvirt-daemon-driver-storage-rbd, sysstat, python3-libnbd, socat
Depends: ${python:Depends}, ${python3:Depends}, openjdk-17-jre-headless | java17-runtime-headless | java17-runtime | zulu-17, cloudstack-common (= ${source:Version}), lsb-base (>= 9), openssh-client, qemu-kvm (>= 2.5) | qemu-system-x86 (>= 5.2), libvirt-bin (>= 1.3) | libvirt-daemon-system (>= 3.0), iproute2, ebtables, vlan, ipset, python3-libvirt, ethtool, iptables, cryptsetup, rng-tools, rsync, ovmf, swtpm, lsb-release, ufw, apparmor, cpu-checker, libvirt-daemon-driver-storage-rbd, sysstat, python3-libnbd, socat, openssl, bzip2, gzip, unzip
Recommends: init-system-helpers
Conflicts: cloud-agent, cloud-agent-libs, cloud-agent-deps, cloud-agent-scripts
Description: CloudStack agent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,12 @@ List<DiskProfile> allocateTemplatedVolumes(Type type, String name, DiskOffering
*/
DiskProfile importVolume(Type type, String name, DiskOffering offering, Long sizeInBytes, Long minIops, Long maxIops,
Long zoneId, HypervisorType hypervisorType, VirtualMachine vm, VirtualMachineTemplate template,
Account owner, Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo);
Account owner, Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo,
Storage.ImageFormat format);

DiskProfile updateImportedVolume(Type type, DiskOffering offering, VirtualMachine vm, VirtualMachineTemplate template,
Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo, DiskProfile diskProfile);
Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo, DiskProfile diskProfile,
Storage.ImageFormat format);

/**
* Unmanage VM volumes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
*/
public interface HighAvailabilityManager extends Manager {

List<StoragePoolType> LIBVIRT_STORAGE_POOL_TYPES_WITH_HA_SUPPORT = List.of(StoragePoolType.NetworkFilesystem, StoragePoolType.SharedMountPoint);
List<StoragePoolType> LIBVIRT_STORAGE_POOL_TYPES_WITH_HA_SUPPORT = List.of(StoragePoolType.NetworkFilesystem, StoragePoolType.SharedMountPoint,
StoragePoolType.RBD);

ConfigKey<Boolean> ForceHA = new ConfigKey<>("Advanced", Boolean.class, "force.ha", "false",
"Force High-Availability to happen even if the VM says no.", true, Cluster);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2626,7 +2626,8 @@ public void updateVolumeDiskChain(long volumeId, String path, String chainInfo,
@Override
public DiskProfile importVolume(Type type, String name, DiskOffering offering, Long sizeInBytes, Long minIops, Long maxIops,
Long zoneId, HypervisorType hypervisorType, VirtualMachine vm, VirtualMachineTemplate template, Account owner,
Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo) {
Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo,
ImageFormat format) {
if (sizeInBytes == null) {
sizeInBytes = offering.getDiskSize();
}
Expand Down Expand Up @@ -2665,7 +2666,9 @@ public DiskProfile importVolume(Type type, String name, DiskOffering offering, L
vol.setDisplayVolume(userVm.isDisplayVm());
}

vol.setFormat(getSupportedImageFormatForCluster(hypervisorType));
// The format the hypervisor actually reported for the existing image wins; pools such as RBD
// hold raw images even though QCOW2 is the cluster default for KVM.
vol.setFormat(format != null ? format : getSupportedImageFormatForCluster(hypervisorType));
vol.setPoolId(poolId);
vol.setPoolType(poolType);
vol.setPath(path);
Expand All @@ -2677,7 +2680,8 @@ public DiskProfile importVolume(Type type, String name, DiskOffering offering, L

@Override
public DiskProfile updateImportedVolume(Type type, DiskOffering offering, VirtualMachine vm, VirtualMachineTemplate template,
Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo, DiskProfile diskProfile) {
Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo, DiskProfile diskProfile,
ImageFormat format) {

VolumeVO vol = _volsDao.findById(diskProfile.getVolumeId());
if (vm != null) {
Expand Down Expand Up @@ -2709,7 +2713,9 @@ public DiskProfile updateImportedVolume(Type type, DiskOffering offering, Virtua
vol.setDisplayVolume(userVm.isDisplayVm());
}

vol.setFormat(getSupportedImageFormatForCluster(vm.getHypervisorType()));
// The format the hypervisor actually reported for the existing image wins; pools such as RBD
// hold raw images even though QCOW2 is the cluster default for KVM.
vol.setFormat(format != null ? format : getSupportedImageFormatForCluster(vm.getHypervisorType()));
vol.setPoolId(poolId);
vol.setPoolType(poolType);
vol.setPath(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ public void testImportVolume() {

volumeOrchestrator.importVolume(volumeType, name, diskOffering, sizeInBytes, null, null,
zoneId, hypervisorType, null, null, owner,
deviceId, poolId, Storage.StoragePoolType.NetworkFilesystem, path, chainInfo);
deviceId, poolId, Storage.StoragePoolType.NetworkFilesystem, path, chainInfo, null);

VolumeVO volume = volumeVOMockedConstructionConstruction.constructed().get(0);
Mockito.verify(volume, Mockito.never()).setInstanceId(Mockito.anyLong());
Expand Down
4 changes: 4 additions & 0 deletions packaging/el8/cloud.spec
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ Requires: (selinux-tools if selinux-tools)
Requires: sysstat
Requires: python3-libnbd
Requires: socat
Requires: openssl
Requires: bzip2
Requires: gzip
Requires: unzip
Provides: cloud-agent
Group: System Environment/Libraries
%description agent
Expand Down
4 changes: 4 additions & 0 deletions packaging/suse15/cloud.spec
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ Requires: rng-tools
Requires: (libgcrypt > 1.8.3 or libgcrypt20)
Requires: (selinux-tools if selinux-tools)
Requires: sysstat
Requires: openssl
Requires: bzip2
Requires: gzip
Requires: unzip
Provides: cloud-agent
Group: System Environment/Libraries
%description agent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ private void checkForNotExistingLibvirtStoragePools(Set<String> removedPools, St
removedPools.add(uuid);
}

logger.debug("Found NFS storage pool [{}] in libvirt, continuing.", uuid);
logger.debug("Found storage pool [{}] in libvirt, continuing.", uuid);

} catch (LibvirtException e) {
logger.debug("Failed to lookup libvirt storage pool [{}].", uuid, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static com.cloud.host.Host.HOST_CDROM_MAX_COUNT;
import static com.cloud.host.Host.HOST_INSTANCE_CONVERSION;
import static com.cloud.host.Host.HOST_OVFTOOL_VERSION;
import static com.cloud.host.Host.HOST_RBD_VOLUME_ENCRYPTION;
import static com.cloud.host.Host.HOST_VDDK_LIB_DIR;
import static com.cloud.host.Host.HOST_VDDK_SUPPORT;
import static com.cloud.host.Host.HOST_VDDK_VERSION;
Expand Down Expand Up @@ -91,6 +92,7 @@
import org.apache.cloudstack.storage.volume.VolumeOnStorageTO;
import org.apache.cloudstack.utils.bytescale.ByteScaleUtils;
import org.apache.cloudstack.utils.cryptsetup.CryptSetup;
import org.apache.cloudstack.utils.rbd.RbdEncryption;
import org.apache.cloudstack.utils.hypervisor.HypervisorUtils;
import org.apache.cloudstack.utils.linux.CPUStat;
import org.apache.cloudstack.utils.linux.KVMHostInfo;
Expand Down Expand Up @@ -3882,7 +3884,9 @@ public int compare(final DiskTO arg0, final DiskTO arg1) {
if (volumeObjectTO.requiresEncryption() &&
pool.getType().encryptionSupportMode() == Storage.EncryptionSupport.Hypervisor ) {
String secretUuid = createLibvirtVolumeSecret(conn, volumeObjectTO.getPath(), volumeObjectTO.getPassphrase());
DiskDef.LibvirtDiskEncryptDetails encryptDetails = new DiskDef.LibvirtDiskEncryptDetails(secretUuid, QemuObject.EncryptFormat.enumValue(volumeObjectTO.getEncryptFormat()));
// RBD volumes are encrypted natively by librbd, so request the librbd encryption engine.
String encryptEngine = (pool.getType() == StoragePoolType.RBD) ? "librbd" : null;
DiskDef.LibvirtDiskEncryptDetails encryptDetails = new DiskDef.LibvirtDiskEncryptDetails(secretUuid, QemuObject.EncryptFormat.enumValue(volumeObjectTO.getEncryptFormat()), encryptEngine);
disk.setLibvirtDiskEncryptDetails(encryptDetails);
}
}
Expand Down Expand Up @@ -4410,6 +4414,7 @@ public StartupCommand[] initialize() {
cmd.setGatewayIpAddress(localGateway);
cmd.setIqn(getIqn());
cmd.getHostDetails().put(HOST_VOLUME_ENCRYPTION, String.valueOf(hostSupportsVolumeEncryption()));
cmd.getHostDetails().put(HOST_RBD_VOLUME_ENCRYPTION, String.valueOf(hostSupportsRbdVolumeEncryption()));
cmd.setHostTags(getHostTags());
boolean instanceConversionSupported = hostSupportsInstanceConversion();
cmd.getHostDetails().put(HOST_INSTANCE_CONVERSION, String.valueOf(instanceConversionSupported));
Expand Down Expand Up @@ -6195,7 +6200,10 @@ public boolean isHostSecured() {
}

/**
* Test host for volume encryption support
* Test host for qemu-native LUKS volume encryption (qemu-img LUKS support + cryptsetup),
* reported as {@code host.volume.encryption}. RBD/librbd encryption support is a separate
* capability, reported as {@code host.volume.encryption.rbd}
* (see {@link #hostSupportsRbdVolumeEncryption()}).
* @return boolean
*/
public boolean hostSupportsVolumeEncryption() {
Expand All @@ -6220,6 +6228,13 @@ public boolean hostSupportsVolumeEncryption() {
return true;
}

/**
* Test host for librbd native LUKS encryption support (rbd CLI with the encryption subcommand).
*/
public boolean hostSupportsRbdVolumeEncryption() {
return new RbdEncryption().isSupported();
}

public boolean isSecureMode(String bootMode) {
if (StringUtils.isNotBlank(bootMode) && "secure".equalsIgnoreCase(bootMode)) {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -788,14 +788,21 @@ public static class DiskDef {
public static class LibvirtDiskEncryptDetails {
String passphraseUuid;
QemuObject.EncryptFormat encryptFormat;
String engine; // optional libvirt encryption engine (e.g. "librbd"); null => libvirt/qemu default

public LibvirtDiskEncryptDetails(String passphraseUuid, QemuObject.EncryptFormat encryptFormat) {
this(passphraseUuid, encryptFormat, null);
}

public LibvirtDiskEncryptDetails(String passphraseUuid, QemuObject.EncryptFormat encryptFormat, String engine) {
this.passphraseUuid = passphraseUuid;
this.encryptFormat = encryptFormat;
this.engine = engine;
}

public String getPassphraseUuid() { return this.passphraseUuid; }
public QemuObject.EncryptFormat getEncryptFormat() { return this.encryptFormat; }
public String getEngine() { return this.engine; }
}

public static class DiskGeometry {
Expand Down Expand Up @@ -1446,7 +1453,11 @@ public String toString() {
}

if (encryptDetails != null) {
diskBuilder.append("<encryption format='" + encryptDetails.encryptFormat + "'>\n");
diskBuilder.append("<encryption format='" + encryptDetails.encryptFormat + "'");
if (encryptDetails.engine != null) {
diskBuilder.append(" engine='" + encryptDetails.engine + "'");
}
diskBuilder.append(">\n");
diskBuilder.append("<secret type='passphrase' uuid='" + encryptDetails.passphraseUuid + "' />\n");
diskBuilder.append("</encryption>\n");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ public final class LibvirtCheckVolumeCommandWrapper extends CommandWrapper<Check
private static final List<Storage.StoragePoolType> STORAGE_POOL_TYPES_SUPPORTED = Arrays.asList(
Storage.StoragePoolType.Filesystem,
Storage.StoragePoolType.NetworkFilesystem,
Storage.StoragePoolType.SharedMountPoint);
Storage.StoragePoolType.SharedMountPoint,
Storage.StoragePoolType.RBD);

@Override
public Answer execute(final CheckVolumeCommand command, final LibvirtComputingResource libvirtComputingResource) {
Expand All @@ -64,14 +65,25 @@ public Answer execute(final CheckVolumeCommand command, final LibvirtComputingRe
if (STORAGE_POOL_TYPES_SUPPORTED.contains(storageFilerTO.getType())) {
final KVMPhysicalDisk vol = pool.getPhysicalDisk(srcFile);
final String path = vol.getPath();
try {
KVMPhysicalDisk.checkQcow2File(path);
} catch (final CloudRuntimeException e) {
return new CheckVolumeAnswer(command, false, "", 0, getVolumeDetails(pool, vol));
final boolean isRbd = Storage.StoragePoolType.RBD.equals(storageFilerTO.getType());

Map<VolumeOnStorageTO.Detail, String> volumeDetails = getVolumeDetails(pool, vol);
if (MapUtils.isEmpty(volumeDetails)) {
return new Answer(command, false, "Unable to read the volume on the storage pool");
}

if (!isRbd) {
try {
KVMPhysicalDisk.checkQcow2File(path);
} catch (final CloudRuntimeException e) {
return new CheckVolumeAnswer(command, false, "", 0, volumeDetails);
}
}

long size = KVMPhysicalDisk.getVirtualSizeFromFile(path);
return new CheckVolumeAnswer(command, true, "", size, getVolumeDetails(pool, vol));
// 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);
return new CheckVolumeAnswer(command, true, "", size, volumeDetails);
} else {
return new Answer(command, false, "Unsupported Storage Pool");
}
Expand Down Expand Up @@ -122,6 +134,9 @@ private Map<String, String> getDiskFileInfo(KVMStoragePool pool, KVMPhysicalDisk
try {
QemuImg qemu = new QemuImg(0);
QemuImgFile qemuFile = new QemuImgFile(disk.getPath(), disk.getFormat());
if (Storage.StoragePoolType.RBD.equals(pool.getType())) {
qemuFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(pool, disk.getPath()), disk.getFormat());
}
return qemu.info(qemuFile, secure);
} catch (QemuImgException | LibvirtException ex) {
logger.error("Failed to get info of disk file: " + ex.getMessage());
Expand Down
Loading