Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
adfd56b
smoke: skip the encrypted RBD hot-plug tests on hosts with libvirt ol…
weizhouapache Nov 13, 2026
7078487
Add CLVM pool types to the bypass sec store list
Pearl1594 Nov 13, 2026
2017b00
Fix NPE when cleaning up dest volumes of a failed live migration with…
weizhouapache Nov 13, 2026
7e832c4
KVM: live migrate Instances with their volumes between RBD, NFS, Shar…
weizhouapache Nov 13, 2026
f46e8d8
KVM: live migrate Instances with their volumes from RBD to CLVM and C…
weizhouapache Nov 13, 2026
a8deec4
Expunge a dest volume already in Destroy without resending DestroyReq…
weizhouapache Nov 13, 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
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
// 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;
package com.cloud.storage;

import java.util.Map;

class StoragePoolInformation {
/**
* What an agent needs to know to reach a primary storage pool: for a pool that is not (yet) defined on the host, e.g. a
* pool of another cluster to which an Instance is migrated.
*/
public class StoragePoolInformation {
private String name;
private String host;
private int port;
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));
}
}
}
}
19 changes: 17 additions & 2 deletions core/src/main/java/com/cloud/agent/api/MigrateCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.cloud.agent.api.to.DpdkTO;
import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.storage.Storage;
import com.cloud.storage.StoragePoolInformation;

public class MigrateCommand extends Command {
private String vmName;
Expand Down Expand Up @@ -161,7 +162,7 @@ public void setClvmCrossPoolMigration(boolean clvmCrossPoolMigration) {

public static class MigrateDiskInfo {
public enum DiskType {
FILE, BLOCK;
FILE, BLOCK, NETWORK;

@Override
public String toString() {
Expand All @@ -179,7 +180,7 @@ public String toString() {
}

public enum Source {
FILE, DEV;
FILE, DEV, NETWORK;

@Override
public String toString() {
Expand All @@ -196,6 +197,7 @@ public String toString() {
private boolean isSourceDiskOnStorageFileSystem;
private Storage.StoragePoolType sourcePoolType;
private Storage.StoragePoolType destPoolType;
private StoragePoolInformation destPoolInformation;

public MigrateDiskInfo(final String serialNumber, final DiskType diskType, final DriverType driverType, final Source source, final String sourceText) {
this.serialNumber = serialNumber;
Expand Down Expand Up @@ -260,6 +262,19 @@ public Storage.StoragePoolType getDestPoolType() {
public void setDestPoolType(Storage.StoragePoolType destPoolType) {
this.destPoolType = destPoolType;
}

/**
* Describes the destination pool of a {@link DiskType#NETWORK} disk: the source host cannot look it up, as the
* pool may belong to a cluster it is not part of. The name is the UUID of the pool, the host holds the
* (comma separated) monitors and the user info only the user, so that no secret travels with the command.
*/
public void setDestPoolInformation(StoragePoolInformation destPoolInformation) {
this.destPoolInformation = destPoolInformation;
}

public StoragePoolInformation getDestPoolInformation() {
return destPoolInformation;
}
}

@Override
Expand Down
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,7 +95,10 @@ public class AncientDataMotionStrategy implements DataMotionStrategy {
private static final List<StoragePoolType> SUPPORTED_POOL_TYPES_TO_BYPASS_SECONDARY_STORE = Arrays.asList(
StoragePoolType.NetworkFilesystem,
StoragePoolType.Filesystem,
StoragePoolType.RBD
StoragePoolType.SharedMountPoint,
StoragePoolType.RBD,
StoragePoolType.CLVM,
StoragePoolType.CLVM_NG
);

@Inject
Expand Down Expand Up @@ -642,6 +645,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 +680,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 @@ -44,6 +44,7 @@
import com.cloud.exception.OperationTimedoutException;
import com.cloud.host.Host;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.storage.clvm.ClvmPoolManager;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.ScopeType;
import com.cloud.storage.Storage;
Expand Down Expand Up @@ -90,13 +91,35 @@ protected StrategyPriority internalCanHandle(Map<VolumeInfo, DataStore> volumeMa
Set<VolumeInfo> volumeInfoSet = volumeMap.keySet();
for (VolumeInfo volumeInfo : volumeInfoSet) {
StoragePoolVO storagePoolVO = _storagePoolDao.findById(volumeInfo.getPoolId());
if (!supportStoragePoolType(storagePoolVO.getPoolType())) {
if (!supportStoragePoolType(storagePoolVO.getPoolType()) && !isRbdSourceSupported(storagePoolVO, volumeMap.get(volumeInfo))) {
return StrategyPriority.CANT_HANDLE;
}
}
return StrategyPriority.HYPERVISOR;
}

/**
* True if the volume lives on an RBD pool and goes to an RBD, NFS, SharedMountPoint, local or CLVM/CLVM_NG pool, which can be in
* another cluster. The Instance is live migrated with its disk copied over NBD to an image or file on the
* destination pool. A volume which stays on its
* pool is not copied (see {@link #shouldMigrateVolume}).
*/
protected boolean isRbdSourceSupported(StoragePoolVO sourcePool, DataStore destDataStore) {
if (sourcePool == null || destDataStore == null || sourcePool.getPoolType() != StoragePoolType.RBD) {
return false;
}
StoragePoolVO destPool = _storagePoolDao.findById(destDataStore.getId());
return destPool != null && isDestinationOfRbdSource(destPool.getPoolType());
}

/**
* An RBD volume can be copied to an RBD, NFS, SharedMountPoint, local or CLVM/CLVM_NG pool.
*/
protected boolean isDestinationOfRbdSource(StoragePoolType destPoolType) {
return destPoolType == StoragePoolType.RBD || ClvmPoolManager.isClvmPoolType(destPoolType)
|| supportStoragePoolType(destPoolType, StoragePoolType.Filesystem);
}

/**
* Allow KVM live storage migration for non managed storage when:
* - Source host and destination host are different, and are on the same cluster
Expand Down Expand Up @@ -173,6 +196,10 @@ protected String generateDestPath(Host destHost, StoragePoolVO destStoragePool,
return String.format("/dev/%s/%s", vgName, destVolumeInfo.getUuid());
}

if (destStoragePool.getPoolType() == StoragePoolType.RBD) {
return String.format("%s/%s", destStoragePool.getPath(), destVolumeInfo.getUuid());
}

return new File(destStoragePool.getPath(), destVolumeInfo.getUuid()).getAbsolutePath();
}

Expand Down Expand Up @@ -210,7 +237,9 @@ protected void setVolumePath(VolumeVO volume) {
*/
@Override
protected boolean shouldMigrateVolume(StoragePoolVO sourceStoragePool, Host destHost, StoragePoolVO destStoragePool) {
return supportStoragePoolType(sourceStoragePool.getPoolType());
return supportStoragePoolType(sourceStoragePool.getPoolType())
|| (sourceStoragePool.getPoolType() == StoragePoolType.RBD && sourceStoragePool.getId() != destStoragePool.getId()
&& isDestinationOfRbdSource(destStoragePool.getPoolType()));
}

/**
Expand Down
Loading
Loading