Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
efe1db7
utils: add RbdUtils for the Ceph monitors of an RBD pool
weizhouapache Oct 14, 2026
6cc5a18
KVM: live volume migration on ceph
weizhouapache Oct 7, 2026
187ee3c
smoke tests: exercise online volume migration for KVM on RBD
weizhouapache Oct 7, 2026
b62125c
KVM: live migrate volumes between RBD, NFS, SharedMountPoint and loca…
weizhouapache Oct 15, 2026
2563acd
KVM: copy a volume of a SharedMountPoint pool directly between primar…
weizhouapache Oct 19, 2026
abdaa03
KVM: do not fail a volume migration whose block copy was pivoted
weizhouapache Oct 19, 2026
dac5d81
KVM: leave the format detection of libvirt volumes and the pool refre…
weizhouapache Oct 19, 2026
c8b302d
KVM: do not name an RBD volume copied between primary storages after …
weizhouapache Oct 19, 2026
3a1a22c
KVM: limit live volume migration to RBD pools and RBD to file based p…
weizhouapache Oct 19, 2026
c47fce6
Merge remote-tracking branch 'apache/main' into 24-ceph-volumes-migra…
weizhouapache Oct 26, 2026
0f2fc95
Merge remote-tracking branch 'apache/main' into 24-ceph-volumes-migra…
weizhouapache Oct 28, 2026
a85fdac
kvm: print the libvirt version of two errors as major.minor.release
weizhouapache Nov 10, 2026
0ab9030
KVM: refuse a live migration into an encrypted RBD volume on libvirt …
weizhouapache Nov 10, 2026
7d2f283
kvm: do not copy a qcow2 file into a RAW destination
weizhouapache Nov 12, 2026
3432be9
kvm: read the qcow2 copy of a volume on secondary storage as qcow2
weizhouapache Nov 12, 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
42 changes: 42 additions & 0 deletions api/src/main/java/com/cloud/storage/Storage.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public class Storage {
public static enum ImageFormat {
Expand Down Expand Up @@ -226,6 +227,47 @@ public static List<StoragePoolType> getNonSharedStoragePoolTypes() {

}

// pool types whose volumes are files, on KVM
public static final Set<StoragePoolType> KVM_FILE_BASED_POOL_TYPES = Set.of(StoragePoolType.NetworkFilesystem, StoragePoolType.SharedMountPoint,
StoragePoolType.Filesystem);

/**
* True if a volume is migrated between two pools of the same type.
*/
public static boolean arePoolsSameType(StoragePoolType srcType, StoragePoolType destType) {
return srcType != null && srcType.equals(destType);
}

/**
* True if at least one of the pools is file based (NFS, SharedMountPoint or local) on KVM, whether it is the
* source or the destination, and whether the other one is file based as well.
*/
public static boolean isAnyPoolFileBased(StoragePoolType srcType, StoragePoolType destType) {
return KVM_FILE_BASED_POOL_TYPES.contains(srcType) || KVM_FILE_BASED_POOL_TYPES.contains(destType);
}

/**
* True if KVM copies a volume in place with a libvirt block copy: between two RBD pools, or between an RBD pool and
* a file based pool (NFS, SharedMountPoint or local). Both pools are then reachable from the host the Instance runs
* on, so the volume of a running Instance can be copied without moving the Instance.
*/
public static boolean isCopiedInPlaceOnKvm(StoragePoolType srcType, StoragePoolType destType) {
if (!StoragePoolType.RBD.equals(srcType) && !StoragePoolType.RBD.equals(destType)) {
return false;
}
return arePoolsSameType(srcType, destType) || isAnyPoolFileBased(srcType, destType);
}

/**
* True if a volume is migrated between two file based pools (NFS, SharedMountPoint or local) on KVM. Such a volume
* is copied with qemu-img, which reads and writes an encrypted volume with its passphrase, so it is not limited to
* the pools a libvirt block copy can reach. It is the way a volume is moved when it is not in use by a running
* Instance.
*/
public static boolean areBothPoolsFileBased(StoragePoolType srcType, StoragePoolType destType) {
return KVM_FILE_BASED_POOL_TYPES.contains(srcType) && KVM_FILE_BASED_POOL_TYPES.contains(destType);
}

public static enum StorageResourceType {
STORAGE_POOL, STORAGE_HOST, SECONDARY_STORAGE, LOCAL_SECONDARY_STORAGE
}
Expand Down
78 changes: 78 additions & 0 deletions api/src/test/java/com/cloud/storage/StorageTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,82 @@ public void equalityTest() {
Assert.assertTrue(t1.equals(StoragePoolType.NetworkFilesystem));
Assert.assertFalse(t1.equals(StoragePoolType.EXT));
}

@Test
public void arePoolsSameTypeForTwoPoolsOfThatType() {
for (StoragePoolType poolType : StoragePoolType.values()) {
Assert.assertTrue(poolType + " -> " + poolType, Storage.arePoolsSameType(poolType, poolType));
for (StoragePoolType other : StoragePoolType.values()) {
if (other.equals(poolType)) {
continue;
}
Assert.assertFalse(poolType + " -> " + other, Storage.arePoolsSameType(poolType, other));
}
}
}

@Test
public void isAnyPoolFileBasedForOneFileBasedPool() {
for (StoragePoolType fileBased : Storage.KVM_FILE_BASED_POOL_TYPES) {
Assert.assertTrue("RBD -> " + fileBased, Storage.isAnyPoolFileBased(StoragePoolType.RBD, fileBased));
Assert.assertTrue(fileBased + " -> RBD", Storage.isAnyPoolFileBased(fileBased, StoragePoolType.RBD));
for (StoragePoolType otherFileBased : Storage.KVM_FILE_BASED_POOL_TYPES) {
Assert.assertTrue(fileBased + " -> " + otherFileBased, Storage.isAnyPoolFileBased(fileBased, otherFileBased));
}
}
Assert.assertFalse("RBD -> RBD", Storage.isAnyPoolFileBased(StoragePoolType.RBD, StoragePoolType.RBD));
}

@Test
public void isCopiedInPlaceOnKvmForRbdAndForRbdAndFileBasedPools() {
Assert.assertTrue("RBD -> RBD", Storage.isCopiedInPlaceOnKvm(StoragePoolType.RBD, StoragePoolType.RBD));
for (StoragePoolType fileBased : Storage.KVM_FILE_BASED_POOL_TYPES) {
Assert.assertTrue("RBD -> " + fileBased, Storage.isCopiedInPlaceOnKvm(StoragePoolType.RBD, fileBased));
Assert.assertTrue(fileBased + " -> RBD", Storage.isCopiedInPlaceOnKvm(fileBased, StoragePoolType.RBD));
}
}

@Test
public void isCopiedInPlaceOnKvmNotBetweenTwoFileBasedPools() {
for (StoragePoolType srcType : Storage.KVM_FILE_BASED_POOL_TYPES) {
for (StoragePoolType destType : Storage.KVM_FILE_BASED_POOL_TYPES) {
Assert.assertFalse(srcType + " -> " + destType, Storage.isCopiedInPlaceOnKvm(srcType, destType));
}
}
}

@Test
public void isCopiedInPlaceOnKvmNotWithOtherPoolTypes() {
for (StoragePoolType other : StoragePoolType.values()) {
if (StoragePoolType.RBD.equals(other) || Storage.KVM_FILE_BASED_POOL_TYPES.contains(other)) {
continue;
}
Assert.assertFalse(other + " -> " + other, Storage.isCopiedInPlaceOnKvm(other, other));
Assert.assertFalse("RBD -> " + other, Storage.isCopiedInPlaceOnKvm(StoragePoolType.RBD, other));
Assert.assertFalse(other + " -> RBD", Storage.isCopiedInPlaceOnKvm(other, StoragePoolType.RBD));
}
}

@Test
public void areBothPoolsFileBasedBetweenTwoFileBasedPools() {
for (StoragePoolType srcType : Storage.KVM_FILE_BASED_POOL_TYPES) {
for (StoragePoolType destType : Storage.KVM_FILE_BASED_POOL_TYPES) {
Assert.assertTrue(srcType + " -> " + destType, Storage.areBothPoolsFileBased(srcType, destType));
}
}
}

@Test
public void areBothPoolsFileBasedNotWithOtherPoolTypes() {
for (StoragePoolType other : StoragePoolType.values()) {
if (Storage.KVM_FILE_BASED_POOL_TYPES.contains(other)) {
continue;
}
Assert.assertFalse(other + " -> " + other, Storage.areBothPoolsFileBased(other, other));
for (StoragePoolType fileBased : Storage.KVM_FILE_BASED_POOL_TYPES) {
Assert.assertFalse(fileBased + " -> " + other, Storage.areBothPoolsFileBased(fileBased, other));
Assert.assertFalse(other + " -> " + fileBased, Storage.areBothPoolsFileBased(other, fileBased));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ public DataTO getDestData() {
return destData;
}

public void setSrcData(DataTO srcData) {
this.srcData = srcData;
}

public void setDestData(DataTO destData) {
this.destData = destData;
}

public void setSrcDetails(Map<String, String> details) {
srcDetails = details;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public class AncientDataMotionStrategy implements DataMotionStrategy {
private static final List<StoragePoolType> SUPPORTED_POOL_TYPES_TO_BYPASS_SECONDARY_STORE = Arrays.asList(
StoragePoolType.NetworkFilesystem,
StoragePoolType.Filesystem,
StoragePoolType.SharedMountPoint,
StoragePoolType.RBD
);

Expand Down Expand Up @@ -642,6 +643,8 @@ protected Answer migrateVolumeToPool(DataObject srcData, DataObject destData) {
command.setContextParam(DiskTO.PROTOCOL_TYPE, Storage.StoragePoolType.DatastoreCluster.toString());
}

command.setSrcData(srcData.getTO());
command.setDestData(destData.getTO());
boolean encryptionRequired = anyVolumeRequiresEncryption(srcData, destData);

EndPoint ep = selector.select(srcData, StorageAction.MIGRATEVOLUME);
Expand Down Expand Up @@ -675,6 +678,21 @@ protected Answer migrateVolumeToPool(DataObject srcData, DataObject destData) {
volumeVo.setLastPoolId(oldPoolId);
if (destPool.getPoolType() == StoragePoolType.CLVM) {
volumeVo.setFormat(ImageFormat.RAW);
} else if (destPool.getPoolType() == StoragePoolType.RBD) {
// RBD only ever stores raw images; keep the volume's format metadata consistent
// regardless of what format it had on the source pool (e.g. QCOW2 on NFS).
volumeVo.setFormat(ImageFormat.RAW);
} else if (srcPool.getPoolType() == StoragePoolType.RBD && volumeVo.getFormat() == ImageFormat.RAW
&& Storage.KVM_FILE_BASED_POOL_TYPES.contains(destPool.getPoolType())) {
// The KVM agent (LibvirtMigrateVolumeCommandWrapper) writes a real QCOW2 file when the
// destination is file-based; without updating this, the volume keeps looking RAW to
// LibvirtComputingResource, which then (mis)treats a plain qcow2 file on NFS as a raw
// block device the next time the Instance is started.
volumeVo.setFormat(ImageFormat.QCOW2);
}
if (volumeVo.getEncryptFormat() != null && (srcPool.getPoolType() == StoragePoolType.RBD) != (destPool.getPoolType() == StoragePoolType.RBD)) {
// The KVM agent encrypts an RBD volume natively with librbd (LUKS2), and a volume on any other pool with qemu (LUKS)
volumeVo.setEncryptFormat(destPool.getPoolType() == StoragePoolType.RBD ? "luks2" : "luks");
}
if (ClvmPoolManager.isClvmPoolType(destPool.getPoolType())) {
clvmPoolManager.setClvmLockHostId(volume.getId(), ep.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ public EndPoint select(DataObject object, StorageAction action, boolean encrypti
}
case MIGRATEVOLUME: {
VolumeInfo volume = (VolumeInfo) object;
if (volume.getHypervisorType() == Hypervisor.HypervisorType.Hyperv || volume.getHypervisorType() == Hypervisor.HypervisorType.VMware) {
if (List.of(Hypervisor.HypervisorType.VMware, Hypervisor.HypervisorType.Hyperv, Hypervisor.HypervisorType.KVM).contains(volume.getHypervisorType())) {
VirtualMachine vm = volume.getAttachedVM();
if ((vm != null) && (vm.getState() == VirtualMachine.State.Running)) {
Long hostId = vm.getHostId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1424,7 +1424,7 @@ public boolean configure(final String name, final Map<String, Object> params) th

if (this.bridgeType == BridgeType.OPENVSWITCH) {
if (conn.getLibVirVersion() < 10 * 1000 + 0) {
throw new ConfigurationException("Libvirt version 0.10.0 required for openvswitch support, but version " + conn.getLibVirVersion() + " detected");
throw new ConfigurationException("Libvirt version 0.10.0 required for openvswitch support, but version " + prettyVersion(conn.getLibVirVersion()) + " detected");
}
}
} catch (final LibvirtException e) {
Expand Down Expand Up @@ -6104,7 +6104,7 @@ public Pair<Double, Double> getNicStats(final String nicName) {
}
}

private String prettyVersion(final long version) {
public static String prettyVersion(final long version) {
final long major = version / 1000000;
final long minor = version % 1000000 / 1000;
final long release = version % 1000000 % 1000;
Expand Down
Loading
Loading