Skip to content

Commit ac8d69c

Browse files
authored
importVM improvements: RBD support, volume format, and cluster selection (#14162)
* kvm: allow importVm importsource=shared from an RBD pool The CheckVolumeCommand wrapper on the KVM agent only accepted file based pools, so importing a root disk straight from Ceph failed on the agent with "Unsupported Storage Pool" and surfaced as "Disk not found or is invalid" on the management server. Add RBD to the supported pool types, take the virtual size from the disk libvirt resolved (qemu-img cannot open a bare RBD image name), skip the QCOW2 header check for raw RBD images, and build the rbd: URI when running qemu-img info, the same way LibvirtGetVolumesOnStorageCommandWrapper already does for listVolumesForImport. * kvm: record the real image format on an imported volume importVolume and updateImportedVolume both stamped the cluster default format for the hypervisor, so a volume imported from an RBD pool was recorded as QCOW2 while a natively deployed volume on the same pool is RAW. This affected both entry points: importVm importsource=shared for a root disk, and importVolume for a data disk. Pass the format the hypervisor reported for the existing image, from the check answer for a root disk and from the volume listed on the pool for a data disk, and fall back to the hypervisor default only when no format is reported. This also corrects a raw image imported from a file based pool. * vm import: plan the instance inside the pod and cluster of the requested pool importKVMInstanceFromDiskImage planned with the pod and cluster unset, so the planner was free to pick any host in the zone by capacity. When it picked a host in a cluster that cannot see the pool the caller named, the volume check ran against whichever pool that cluster does have, and the import failed with "Disk not found or is invalid" although the image was fine. Take the pod and cluster from the pool the caller passed, the same way importVolume already derives its host from the pool's scope * verify qemu is able to read the rbd image since check can not be done on a raw file * use computed volumeDetails in success path as well. * ui: do not call the imported disk a QCOW2 image The import wizard for local and shared storage described the disk as a QCOW2 image. An image on an RBD pool is raw, and a raw image on a file based pool can be imported as well, so the wording is wrong for both. Call it a disk image instead. Only the English strings are changed; the other locales come from Transifex.
1 parent 273b2ea commit ac8d69c

12 files changed

Lines changed: 182 additions & 34 deletions

File tree

‎api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd {
9292

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

9898
@Parameter(name = ApiConstants.IMPORT_SOURCE,

‎api/src/main/java/org/apache/cloudstack/api/command/admin/volume/ImportVolumeCmd.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public class ImportVolumeCmd extends BaseAsyncCmd {
6363
@Parameter(name = ApiConstants.PATH,
6464
type = BaseCmd.CommandType.STRING,
6565
required = true,
66-
description = "the path of the volume")
66+
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")
6767
private String path;
6868

6969
@Parameter(name = ApiConstants.NAME,

‎engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,12 @@ List<DiskProfile> allocateTemplatedVolumes(Type type, String name, DiskOffering
183183
*/
184184
DiskProfile importVolume(Type type, String name, DiskOffering offering, Long sizeInBytes, Long minIops, Long maxIops,
185185
Long zoneId, HypervisorType hypervisorType, VirtualMachine vm, VirtualMachineTemplate template,
186-
Account owner, Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo);
186+
Account owner, Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo,
187+
Storage.ImageFormat format);
187188

188189
DiskProfile updateImportedVolume(Type type, DiskOffering offering, VirtualMachine vm, VirtualMachineTemplate template,
189-
Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo, DiskProfile diskProfile);
190+
Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo, DiskProfile diskProfile,
191+
Storage.ImageFormat format);
190192

191193
/**
192194
* Unmanage VM volumes

‎engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2328,7 +2328,8 @@ public void updateVolumeDiskChain(long volumeId, String path, String chainInfo,
23282328
@Override
23292329
public DiskProfile importVolume(Type type, String name, DiskOffering offering, Long sizeInBytes, Long minIops, Long maxIops,
23302330
Long zoneId, HypervisorType hypervisorType, VirtualMachine vm, VirtualMachineTemplate template, Account owner,
2331-
Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo) {
2331+
Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo,
2332+
ImageFormat format) {
23322333
if (sizeInBytes == null) {
23332334
sizeInBytes = offering.getDiskSize();
23342335
}
@@ -2367,7 +2368,9 @@ public DiskProfile importVolume(Type type, String name, DiskOffering offering, L
23672368
vol.setDisplayVolume(userVm.isDisplayVm());
23682369
}
23692370

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

23802383
@Override
23812384
public DiskProfile updateImportedVolume(Type type, DiskOffering offering, VirtualMachine vm, VirtualMachineTemplate template,
2382-
Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo, DiskProfile diskProfile) {
2385+
Long deviceId, Long poolId, Storage.StoragePoolType poolType, String path, String chainInfo, DiskProfile diskProfile,
2386+
ImageFormat format) {
23832387

23842388
VolumeVO vol = _volsDao.findById(diskProfile.getVolumeId());
23852389
if (vm != null) {
@@ -2411,7 +2415,9 @@ public DiskProfile updateImportedVolume(Type type, DiskOffering offering, Virtua
24112415
vol.setDisplayVolume(userVm.isDisplayVm());
24122416
}
24132417

2414-
vol.setFormat(getSupportedImageFormatForCluster(vm.getHypervisorType()));
2418+
// The format the hypervisor actually reported for the existing image wins; pools such as RBD
2419+
// hold raw images even though QCOW2 is the cluster default for KVM.
2420+
vol.setFormat(format != null ? format : getSupportedImageFormatForCluster(vm.getHypervisorType()));
24152421
vol.setPoolId(poolId);
24162422
vol.setPoolType(poolType);
24172423
vol.setPath(path);

‎engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestratorTest.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ public void testImportVolume() {
241241

242242
volumeOrchestrator.importVolume(volumeType, name, diskOffering, sizeInBytes, null, null,
243243
zoneId, hypervisorType, null, null, owner,
244-
deviceId, poolId, Storage.StoragePoolType.NetworkFilesystem, path, chainInfo);
244+
deviceId, poolId, Storage.StoragePoolType.NetworkFilesystem, path, chainInfo, null);
245245

246246
VolumeVO volume = volumeVOMockedConstructionConstruction.constructed().get(0);
247247
Mockito.verify(volume, Mockito.never()).setInstanceId(Mockito.anyLong());

‎plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ public final class LibvirtCheckVolumeCommandWrapper extends CommandWrapper<Check
5050
private static final List<Storage.StoragePoolType> STORAGE_POOL_TYPES_SUPPORTED = Arrays.asList(
5151
Storage.StoragePoolType.Filesystem,
5252
Storage.StoragePoolType.NetworkFilesystem,
53-
Storage.StoragePoolType.SharedMountPoint);
53+
Storage.StoragePoolType.SharedMountPoint,
54+
Storage.StoragePoolType.RBD);
5455

5556
@Override
5657
public Answer execute(final CheckVolumeCommand command, final LibvirtComputingResource libvirtComputingResource) {
@@ -64,14 +65,25 @@ public Answer execute(final CheckVolumeCommand command, final LibvirtComputingRe
6465
if (STORAGE_POOL_TYPES_SUPPORTED.contains(storageFilerTO.getType())) {
6566
final KVMPhysicalDisk vol = pool.getPhysicalDisk(srcFile);
6667
final String path = vol.getPath();
67-
try {
68-
KVMPhysicalDisk.checkQcow2File(path);
69-
} catch (final CloudRuntimeException e) {
70-
return new CheckVolumeAnswer(command, false, "", 0, getVolumeDetails(pool, vol));
68+
final boolean isRbd = Storage.StoragePoolType.RBD.equals(storageFilerTO.getType());
69+
70+
Map<VolumeOnStorageTO.Detail, String> volumeDetails = getVolumeDetails(pool, vol);
71+
if (MapUtils.isEmpty(volumeDetails)) {
72+
return new Answer(command, false, "Unable to read the volume on the storage pool");
73+
}
74+
75+
if (!isRbd) {
76+
try {
77+
KVMPhysicalDisk.checkQcow2File(path);
78+
} catch (final CloudRuntimeException e) {
79+
return new CheckVolumeAnswer(command, false, "", 0, volumeDetails);
80+
}
7181
}
7282

73-
long size = KVMPhysicalDisk.getVirtualSizeFromFile(path);
74-
return new CheckVolumeAnswer(command, true, "", size, getVolumeDetails(pool, vol));
83+
// Images on RBD are raw and the path is an image name that qemu-img cannot open
84+
// without the rbd: URI, so take the size libvirt already reported for the volume.
85+
long size = isRbd ? vol.getVirtualSize() : KVMPhysicalDisk.getVirtualSizeFromFile(path);
86+
return new CheckVolumeAnswer(command, true, "", size, volumeDetails);
7587
} else {
7688
return new Answer(command, false, "Unsupported Storage Pool");
7789
}
@@ -122,6 +134,9 @@ private Map<String, String> getDiskFileInfo(KVMStoragePool pool, KVMPhysicalDisk
122134
try {
123135
QemuImg qemu = new QemuImg(0);
124136
QemuImgFile qemuFile = new QemuImgFile(disk.getPath(), disk.getFormat());
137+
if (Storage.StoragePoolType.RBD.equals(pool.getType())) {
138+
qemuFile = new QemuImgFile(KVMPhysicalDisk.RBDStringBuilder(pool, disk.getPath()), disk.getFormat());
139+
}
125140
return qemu.info(qemuFile, secure);
126141
} catch (QemuImgException | LibvirtException ex) {
127142
logger.error("Failed to get info of disk file: " + ex.getMessage());

‎server/src/main/java/org/apache/cloudstack/storage/volume/VolumeImportUnmanageManagerImpl.java‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,10 +461,27 @@ private VolumeVO importVolumeInternal(VolumeOnStorageTO volume, DiskOfferingVO d
461461
Account owner, StoragePoolVO pool, String volumeName) {
462462
DiskProfile diskProfile = volumeManager.importVolume(Volume.Type.DATADISK, volumeName, diskOffering,
463463
volume.getVirtualSize(), null, null, pool.getDataCenterId(), volume.getHypervisorType(), null, null,
464-
owner, null, pool.getId(), pool.getPoolType(), volume.getPath(), null);
464+
owner, null, pool.getId(), pool.getPoolType(), volume.getPath(), null, getImageFormat(volume.getFormat()));
465465
return volumeDao.findById(diskProfile.getVolumeId());
466466
}
467467

468+
/**
469+
* Maps the format the hypervisor reported for the volume on the pool onto an image format, so that
470+
* the imported volume records what is actually on the pool (raw on RBD, qcow2 on file based pools)
471+
* instead of the cluster default for the hypervisor. Returns null when the format is not recognised.
472+
*/
473+
protected Storage.ImageFormat getImageFormat(String format) {
474+
if (StringUtils.isBlank(format)) {
475+
return null;
476+
}
477+
try {
478+
return Storage.ImageFormat.valueOf(format.toUpperCase());
479+
} catch (IllegalArgumentException e) {
480+
logger.warn("Unrecognised image format {} reported for the volume being imported, falling back to the hypervisor default", format);
481+
return null;
482+
}
483+
}
484+
468485
protected void checkResourceLimitForImportVolume(Account owner, VolumeOnStorageTO volume, DiskOfferingVO diskOffering, List<Reserver> reservations) {
469486
Long volumeSize = volume.getVirtualSize();
470487
try {

‎server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java‎

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -796,14 +796,16 @@ private Pair<DiskProfile, StoragePool> importExternalDisk(UnmanagedInstanceTO.Di
796796
}
797797
diskProfile.setSize(copyRemoteVolumeAnswer.getSize());
798798
DiskProfile profile = volumeManager.updateImportedVolume(type, diskOffering, vm, template, deviceId,
799-
storagePool.getId(), storagePool.getPoolType(), copyRemoteVolumeAnswer.getFilename(), chainInfo, diskProfile);
799+
storagePool.getId(), storagePool.getPoolType(), copyRemoteVolumeAnswer.getFilename(), chainInfo, diskProfile,
800+
getImageFormatFromVolumeDetails(copyRemoteVolumeAnswer.getVolumeDetails()));
800801

801802
return new Pair<>(profile, storagePool);
802803
}
803804

804805
private Pair<DiskProfile, StoragePool> importKVMLocalDisk(VirtualMachine vm, DiskOffering diskOffering,
805806
Volume.Type type, VirtualMachineTemplate template,
806-
Long deviceId, Long hostId, String diskPath, DiskProfile diskProfile) {
807+
Long deviceId, Long hostId, String diskPath, DiskProfile diskProfile,
808+
Storage.ImageFormat format) {
807809
List<StoragePoolVO> storagePools = primaryDataStoreDao.findLocalStoragePoolsByHostAndTags(hostId, null);
808810
if(storagePools.size() < 1) {
809811
throw new CloudRuntimeException("Local Storage not found for host");
@@ -812,22 +814,44 @@ private Pair<DiskProfile, StoragePool> importKVMLocalDisk(VirtualMachine vm, Dis
812814
StoragePool storagePool = storagePools.get(0);
813815

814816
DiskProfile profile = volumeManager.updateImportedVolume(type, diskOffering, vm, template, deviceId,
815-
storagePool.getId(), storagePool.getPoolType(), diskPath, null, diskProfile);
817+
storagePool.getId(), storagePool.getPoolType(), diskPath, null, diskProfile, format);
816818

817819
return new Pair<>(profile, storagePool);
818820
}
819821

820822
private Pair<DiskProfile, StoragePool> importKVMSharedDisk(VirtualMachine vm, DiskOffering diskOffering,
821823
Volume.Type type, VirtualMachineTemplate template,
822-
Long deviceId, Long poolId, String diskPath, DiskProfile diskProfile) {
824+
Long deviceId, Long poolId, String diskPath, DiskProfile diskProfile,
825+
Storage.ImageFormat format) {
823826
StoragePool storagePool = primaryDataStoreDao.findById(poolId);
824827

825828
DiskProfile profile = volumeManager.updateImportedVolume(type, diskOffering, vm, template, deviceId,
826-
poolId, storagePool.getPoolType(), diskPath, null, diskProfile);
829+
poolId, storagePool.getPoolType(), diskPath, null, diskProfile, format);
827830

828831
return new Pair<>(profile, storagePool);
829832
}
830833

834+
/**
835+
* Reads the image format the hypervisor reported for an existing volume, so that the imported
836+
* volume records what is actually on the pool (raw on RBD, qcow2 on file based pools) instead of
837+
* the cluster default for the hypervisor. Returns null when the agent did not report a format.
838+
*/
839+
protected Storage.ImageFormat getImageFormatFromVolumeDetails(Map<VolumeOnStorageTO.Detail, String> volumeDetails) {
840+
if (MapUtils.isEmpty(volumeDetails)) {
841+
return null;
842+
}
843+
String fileFormat = volumeDetails.get(VolumeOnStorageTO.Detail.FILE_FORMAT);
844+
if (StringUtils.isBlank(fileFormat)) {
845+
return null;
846+
}
847+
try {
848+
return Storage.ImageFormat.valueOf(fileFormat.toUpperCase());
849+
} catch (IllegalArgumentException e) {
850+
logger.warn("Unrecognised image format {} reported for the volume being imported, falling back to the hypervisor default", fileFormat);
851+
return null;
852+
}
853+
}
854+
831855
private Pair<DiskProfile, StoragePool> importDisk(UnmanagedInstanceTO.Disk disk, VirtualMachine vm, Cluster cluster, DiskOffering diskOffering,
832856
Volume.Type type, String name, Long diskSize, Long minIops, Long maxIops, VirtualMachineTemplate template,
833857
Account owner, Long deviceId) {
@@ -842,7 +866,7 @@ private Pair<DiskProfile, StoragePool> importDisk(UnmanagedInstanceTO.Disk disk,
842866
}
843867
StoragePool storagePool = getStoragePool(disk, zone, cluster, diskOffering);
844868
DiskProfile profile = volumeManager.importVolume(type, name, diskOffering, diskSize,
845-
minIops, maxIops, vm.getDataCenterId(), vm.getHypervisorType(), vm, template, owner, deviceId, storagePool.getId(), storagePool.getPoolType(), path, chainInfo);
869+
minIops, maxIops, vm.getDataCenterId(), vm.getHypervisorType(), vm, template, owner, deviceId, storagePool.getId(), storagePool.getPoolType(), path, chainInfo, null);
846870

847871
return new Pair<DiskProfile, StoragePool>(profile, storagePool);
848872
}
@@ -2647,6 +2671,13 @@ private UserVmResponse importKvmInstance(ImportVmCmd cmd) {
26472671
throw new InvalidParameterValueException("Disk image is already in use");
26482672
}
26492673

2674+
// A host the planner is pinned to must be able to see the pool, otherwise the volume check
2675+
// runs on a host that cannot reach the image and reports it as missing.
2676+
if (ImportSource.SHARED == importSource && hostId != null && storagePoolHostDao.findByPoolHost(poolId, hostId) == null) {
2677+
throw new InvalidParameterValueException(String.format(
2678+
"Specified host does not have access to the storage pool: %s", storagePool.getUuid()));
2679+
}
2680+
26502681
DiskOffering diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId());
26512682

26522683
if (diskOffering != null && !storagePoolSupportsDiskOffering(storagePool, diskOffering)) {
@@ -2923,7 +2954,13 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource,
29232954
ServiceOfferingVO dummyOffering = serviceOfferingDao.findById(userVm.getId(), serviceOffering.getId());
29242955
profile.setServiceOffering(dummyOffering);
29252956
DeploymentPlanner.ExcludeList excludeList = new DeploymentPlanner.ExcludeList();
2926-
final DataCenterDeployment plan = new DataCenterDeployment(zone.getId(), null, null, hostId, poolId, null);
2957+
// Confine the plan to the pod and cluster of the pool the caller asked for. Otherwise the
2958+
// planner is free to pick a host in another cluster that cannot see the pool, and the volume
2959+
// check then runs against whichever pool that cluster does have. Both are null for a zone
2960+
// wide pool, which every host can see.
2961+
StoragePoolVO importStoragePool = primaryDataStoreDao.findById(poolId);
2962+
final DataCenterDeployment plan = new DataCenterDeployment(zone.getId(), importStoragePool.getPodId(),
2963+
importStoragePool.getClusterId(), hostId, poolId, null);
29272964
DeployDestination dest = null;
29282965
try {
29292966
dest = deploymentPlanningManager.planDeployment(profile, plan, excludeList, null);
@@ -2968,12 +3005,13 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource,
29683005
List<Pair<DiskProfile, StoragePool>> diskProfileStoragePoolList = new ArrayList<>();
29693006
try {
29703007
long deviceId = 1L;
3008+
Storage.ImageFormat diskFormat = getImageFormatFromVolumeDetails(checkVolumeAnswer.getVolumeDetails());
29713009
if(ImportSource.SHARED == importSource) {
29723010
diskProfileStoragePoolList.add(importKVMSharedDisk(userVm, diskOffering, Volume.Type.ROOT,
2973-
template, deviceId, poolId, diskPath, diskProfile));
3011+
template, deviceId, poolId, diskPath, diskProfile, diskFormat));
29743012
} else if(ImportSource.LOCAL == importSource) {
29753013
diskProfileStoragePoolList.add(importKVMLocalDisk(userVm, diskOffering, Volume.Type.ROOT,
2976-
template, deviceId, hostId, diskPath, diskProfile));
3014+
template, deviceId, hostId, diskPath, diskProfile, diskFormat));
29773015
}
29783016
} catch (Exception e) {
29793017
logger.error(String.format("Failed to import volumes while importing vm: %s", instanceName), e);

0 commit comments

Comments
 (0)