From 81bb6672671381f46af192b9f46d34217d940c87 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 5 Mar 2026 13:09:56 -0500 Subject: [PATCH] add support for new gen clvm with template (qcow2) backing --- .../main/java/com/cloud/storage/Storage.java | 1 + .../cloud/vm/VirtualMachineManagerImpl.java | 2 +- .../orchestration/VolumeOrchestrator.java | 14 +- .../snapshot/DefaultSnapshotStrategy.java | 2 +- .../endpoint/DefaultEndPointSelector.java | 13 +- .../provider/DefaultHostListener.java | 3 +- .../resource/LibvirtComputingResource.java | 83 +++-- .../LibvirtResizeVolumeCommandWrapper.java | 3 +- .../LibvirtRevertSnapshotCommandWrapper.java | 2 +- .../kvm/storage/KVMStoragePoolManager.java | 33 +- .../kvm/storage/KVMStorageProcessor.java | 27 +- .../kvm/storage/LibvirtStorageAdaptor.java | 339 +++++++++++++++++- .../kvm/storage/LibvirtStoragePool.java | 6 +- ...oudStackPrimaryDataStoreLifeCycleImpl.java | 5 + .../com/cloud/storage/ClvmLockManager.java | 5 + .../vm/snapshot/VMSnapshotManagerImpl.java | 6 + ui/src/views/infra/AddPrimaryStorage.vue | 16 +- 17 files changed, 491 insertions(+), 69 deletions(-) diff --git a/api/src/main/java/com/cloud/storage/Storage.java b/api/src/main/java/com/cloud/storage/Storage.java index 5b3e97698fd..ddf5978497b 100644 --- a/api/src/main/java/com/cloud/storage/Storage.java +++ b/api/src/main/java/com/cloud/storage/Storage.java @@ -170,6 +170,7 @@ public class Storage { ISO(false, false, EncryptionSupport.Unsupported), // for iso image 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 SharedMountPoint(true, true, EncryptionSupport.Hypervisor), VMFS(true, true, EncryptionSupport.Unsupported), // VMware VMFS storage diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index cd5358fc928..8a773f74ab2 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -3382,7 +3382,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac for (VolumeVO volume : volumes) { StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); - if (pool != null && pool.getPoolType() == Storage.StoragePoolType.CLVM) { + if (pool != null && ClvmLockManager.isClvmPoolType(pool.getPoolType())) { clvmLockManager.setClvmLockHostId(volume.getId(), destHostId); } } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index 752e6a08c1a..adf4a723299 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -754,7 +754,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati // For CLVM pools, set the lock host hint so volume is created on the correct host // This avoids the need for shared mode activation and improves performance - if (pool.getPoolType() == Storage.StoragePoolType.CLVM && hostId != null) { + if (ClvmLockManager.isClvmPoolType(pool.getPoolType()) && hostId != null) { logger.info("CLVM pool detected. Setting lock host {} for volume {} to route creation to correct host", hostId, volumeInfo.getUuid()); volumeInfo.setDestinationHostId(hostId); @@ -818,7 +818,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati } StoragePoolVO pool = _storagePoolDao.findById(destPool.getId()); - if (pool == null || pool.getPoolType() != Storage.StoragePoolType.CLVM) { + if (pool == null || pool.getPoolType() != Storage.StoragePoolType.CLVM || pool.getPoolType() != Storage.StoragePoolType.CLVM_NG) { return; } @@ -860,7 +860,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati } StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); - if (pool != null && pool.getPoolType() == Storage.StoragePoolType.CLVM) { + if (pool != null && ClvmLockManager.isClvmPoolType(pool.getPoolType())) { Long lockHostId = clvmLockManager.getClvmLockHostId(volume.getId(), volume.getUuid()); if (lockHostId != null) { logger.debug("Found CLVM lock host {} from existing volume {} of VM {}", @@ -884,7 +884,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati } StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); - if (pool == null || pool.getPoolType() != Storage.StoragePoolType.CLVM) { + if (pool == null || pool.getPoolType() != Storage.StoragePoolType.CLVM || pool.getPoolType() != Storage.StoragePoolType.CLVM_NG) { continue; } @@ -1324,8 +1324,8 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati Long clusterId = storagePool.getClusterId(); logger.trace("storage-pool {}/{} is associated with cluster {}",storagePool.getName(), storagePool.getUuid(), clusterId); Long hostId = vm.getHostId(); - if (hostId == null && (storagePool.isLocal() || storagePool.getPoolType() == Storage.StoragePoolType.CLVM)) { - if (storagePool.getPoolType() == Storage.StoragePoolType.CLVM) { + if (hostId == null && (storagePool.isLocal() || ClvmLockManager.isClvmPoolType(storagePool.getPoolType()))) { + if (ClvmLockManager.isClvmPoolType(storagePool.getPoolType())) { hostId = getClvmLockHostFromVmVolumes(vm.getId()); if (hostId != null) { logger.debug("Using CLVM lock host {} from VM {}'s existing volumes for new volume creation", @@ -1998,7 +1998,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati // For CLVM pools, set the destination host hint so volume is created on the correct host // This avoids the need for shared mode activation and improves performance StoragePoolVO poolVO = _storagePoolDao.findById(destPool.getId()); - if (poolVO != null && poolVO.getPoolType() == Storage.StoragePoolType.CLVM) { + if (poolVO != null && ClvmLockManager.isClvmPoolType(poolVO.getPoolType())) { Long hostId = vm.getVirtualMachine().getHostId(); if (hostId != null) { volume.setDestinationHostId(hostId); diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java index 9c5cea44211..5f41e9d8102 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java @@ -710,7 +710,7 @@ public class DefaultSnapshotStrategy extends SnapshotStrategyBase { } StoragePool pool = (StoragePool) dataStoreMgr.getDataStore(poolId, DataStoreRole.Primary); - if (pool == null || pool.getPoolType() != StoragePoolType.CLVM) { + if (pool == null || pool.getPoolType() != StoragePoolType.CLVM || pool.getPoolType() != StoragePoolType.CLVM_NG) { return false; } diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java index 51f2156d5e3..8393ec6fe63 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/endpoint/DefaultEndPointSelector.java @@ -32,6 +32,7 @@ import javax.inject.Inject; import com.cloud.dc.DedicatedResourceVO; import com.cloud.dc.dao.DedicatedResourceDao; +import com.cloud.storage.ClvmLockManager; import com.cloud.storage.Storage; import com.cloud.storage.VolumeDetailVO; import com.cloud.storage.dao.VolumeDetailsDao; @@ -273,8 +274,6 @@ public class DefaultEndPointSelector implements EndPointSelector { @Override public EndPoint select(DataObject srcData, DataObject destData, boolean volumeEncryptionSupportRequired) { - // FOR CLVM: Check if destination is a volume with destinationHostId hint - // This ensures template-to-volume copy is routed to the correct host for optimal lock placement if (destData instanceof VolumeInfo) { EndPoint clvmEndpoint = selectClvmEndpointIfApplicable((VolumeInfo) destData, "template-to-volume copy"); if (clvmEndpoint != null) { @@ -424,7 +423,9 @@ public class DefaultEndPointSelector implements EndPointSelector { // Check if this is a CLVM pool StoragePoolVO pool = _storagePoolDao.findById(store.getId()); - if (pool == null || pool.getPoolType() != Storage.StoragePoolType.CLVM) { + if (pool == null || + (pool.getPoolType() != Storage.StoragePoolType.CLVM || + pool.getPoolType() != Storage.StoragePoolType.CLVM_NG)) { return null; } @@ -450,7 +451,6 @@ public class DefaultEndPointSelector implements EndPointSelector { public EndPoint select(DataObject object, boolean encryptionSupportRequired) { DataStore store = object.getDataStore(); - // For CLVM volumes with destination host hint, route to that specific host // This ensures volumes are created on the correct host with exclusive locks if (object instanceof VolumeInfo && store.getRole() == DataStoreRole.Primary) { VolumeInfo volInfo = (VolumeInfo) object; @@ -467,6 +467,7 @@ public class DefaultEndPointSelector implements EndPointSelector { throw new CloudRuntimeException(String.format("Storage role %s doesn't support encryption", store.getRole())); } + @Override public EndPoint select(DataObject object) { DataStore store = object.getDataStore(); @@ -475,7 +476,7 @@ public class DefaultEndPointSelector implements EndPointSelector { if (object instanceof VolumeInfo && store.getRole() == DataStoreRole.Primary) { VolumeInfo volume = (VolumeInfo) object; StoragePoolVO pool = _storagePoolDao.findById(store.getId()); - if (pool != null && pool.getPoolType() == Storage.StoragePoolType.CLVM) { + if (pool != null && ClvmLockManager.isClvmPoolType(pool.getPoolType())) { Long lockHostId = getClvmLockHostId(volume); if (lockHostId != null) { logger.debug("Routing CLVM volume {} operation to lock holder host {}", @@ -589,7 +590,7 @@ public class DefaultEndPointSelector implements EndPointSelector { DataStore store = volume.getDataStore(); if (store.getRole() == DataStoreRole.Primary) { StoragePoolVO pool = _storagePoolDao.findById(store.getId()); - if (pool != null && pool.getPoolType() == Storage.StoragePoolType.CLVM) { + if (pool != null && ClvmLockManager.isClvmPoolType(pool.getPoolType())) { Long lockHostId = getClvmLockHostId(volume); if (lockHostId != null) { logger.info("Routing CLVM volume {} deletion to lock holder host {}", diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java index f0629088c8f..4b8c9e3e77a 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/datastore/provider/DefaultHostListener.java @@ -37,6 +37,7 @@ import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; +import com.cloud.storage.ClvmLockManager; import com.cloud.storage.DataStoreRole; import com.cloud.storage.Storage; import com.cloud.storage.StorageManager; @@ -144,7 +145,7 @@ public class DefaultHostListener implements HypervisorHostListener { // Propagate CLVM secure zero-fill setting to the host // Note: This is done during host connection (agent start, MS restart, host reconnection) // so the setting is non-dynamic. Changes require host reconnection to take effect. - if (pool.getPoolType() == Storage.StoragePoolType.CLVM) { + if (ClvmLockManager.isClvmPoolType(pool.getPoolType())) { Boolean clvmSecureZeroFill = VolumeApiServiceImpl.CLVMSecureZeroFill.valueIn(poolId); if (clvmSecureZeroFill != null) { detailsMap.put("clvmsecurezerofill", String.valueOf(clvmSecureZeroFill)); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 92b1c7b4b6a..fbe48d4318c 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -2484,7 +2484,8 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv } else if ((poolType == StoragePoolType.NetworkFilesystem || poolType == StoragePoolType.SharedMountPoint || poolType == StoragePoolType.Filesystem - || poolType == StoragePoolType.Gluster) + || poolType == StoragePoolType.Gluster + || poolType == StoragePoolType.CLVM_NG) && volFormat == PhysicalDiskFormat.QCOW2 ) { return "QCOW2"; } else if (poolType == StoragePoolType.Linstor) { @@ -3680,13 +3681,19 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv final String glusterVolume = pool.getSourceDir().replace("/", ""); disk.defNetworkBasedDisk(glusterVolume + path.replace(mountpoint, ""), pool.getSourceHost(), pool.getSourcePort(), null, null, devId, diskBusType, DiskProtocol.GLUSTER, DiskDef.DiskFmtType.QCOW2); - } else if (pool.getType() == StoragePoolType.CLVM || physicalDisk.getFormat() == PhysicalDiskFormat.RAW) { + } else if (pool.getType() == StoragePoolType.CLVM || pool.getType() == StoragePoolType.CLVM_NG || physicalDisk.getFormat() == PhysicalDiskFormat.RAW) { + // CLVM and CLVM_NG use block devices (/dev/vgname/volume) if (volume.getType() == Volume.Type.DATADISK && !(isWindowsTemplate && isUefiEnabled)) { disk.defBlockBasedDisk(physicalDisk.getPath(), devId, diskBusTypeData); - } - else { + } else { disk.defBlockBasedDisk(physicalDisk.getPath(), devId, diskBusType); } + + // CLVM_NG uses QCOW2 format on block devices, override the default RAW format + if (pool.getType() == StoragePoolType.CLVM_NG) { + disk.setDiskFormatType(DiskDef.DiskFmtType.QCOW2); + } + if (pool.getType() == StoragePoolType.Linstor && isQemuDiscardBugFree(diskBusType)) { disk.setDiscard(DiscardType.UNMAP); } @@ -6566,21 +6573,7 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv if (isClvmVolume(disk, resource, vmSpec)) { String volumePath = disk.getDiskPath(); try { - LOGGER.info("[CLVM Migration] {} for volume [{}]", - state.getLogMessage(), volumePath); - - Script cmd = new Script("lvchange", Duration.standardSeconds(300), LOGGER); - cmd.add(state.getLvchangeFlag()); - cmd.add(volumePath); - - String result = cmd.execute(); - if (result != null) { - LOGGER.error("[CLVM Migration] Failed to set volume [{}] to {} state. Command result: {}", - volumePath, state.getDescription(), result); - } else { - LOGGER.info("[CLVM Migration] Successfully set volume [{}] to {} state.", - volumePath, state.getDescription()); - } + modifyClvmVolumeState(volumePath, state.getLvchangeFlag(), state.getDescription(), state.getLogMessage()); } catch (Exception e) { LOGGER.error("[CLVM Migration] Exception while setting volume [{}] to {} state: {}", volumePath, state.getDescription(), e.getMessage(), e); @@ -6589,6 +6582,53 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv } } + private static void modifyClvmVolumeState(String volumePath, String lvchangeFlag, + String stateDescription, String logMessage) { + try { + LOGGER.info("[CLVM Migration] {} for volume [{}]", logMessage, volumePath); + + Script cmd = new Script("lvchange", Duration.standardSeconds(300), LOGGER); + cmd.add(lvchangeFlag); + cmd.add(volumePath); + + String result = cmd.execute(); + if (result != null) { + String errorMsg = String.format( + "[CLVM Migration] Failed to set volume [%s] to %s state. Command result: %s", + volumePath, stateDescription, result); + LOGGER.error(errorMsg); + throw new CloudRuntimeException(errorMsg); + } else { + LOGGER.info("[CLVM Migration] Successfully set volume [{}] to {} state.", + volumePath, stateDescription); + } + } catch (CloudRuntimeException e) { + throw e; + } catch (Exception e) { + String errorMsg = String.format( + "[CLVM Migration] Exception while setting volume [%s] to %s state: %s", + volumePath, stateDescription, e.getMessage()); + LOGGER.error(errorMsg, e); + throw new CloudRuntimeException(errorMsg, e); + } + } + + public static void activateClvmVolumeExclusive(String volumePath) { + modifyClvmVolumeState(volumePath, ClvmVolumeState.EXCLUSIVE.getLvchangeFlag(), + ClvmVolumeState.EXCLUSIVE.getDescription(), + "Activating CLVM volume in exclusive mode for copy"); + } + + public static void deactivateClvmVolume(String volumePath) { + try { + modifyClvmVolumeState(volumePath, ClvmVolumeState.DEACTIVATE.getLvchangeFlag(), + ClvmVolumeState.DEACTIVATE.getDescription(), + "Deactivating CLVM volume after copy"); + } catch (Exception e) { + LOGGER.warn("Failed to deactivate CLVM volume {}: {}", volumePath, e.getMessage()); + } + } + /** * Determines if a disk is on a CLVM storage pool by checking the actual pool type from VirtualMachineTO. * This is the most reliable method as it uses CloudStack's own storage pool information. @@ -6613,8 +6653,9 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv DataStoreTO dataStore = volumeTO.getDataStore(); if (dataStore instanceof PrimaryDataStoreTO) { PrimaryDataStoreTO primaryStore = (PrimaryDataStoreTO) dataStore; - boolean isClvm = StoragePoolType.CLVM == primaryStore.getPoolType(); - LOGGER.debug("Disk {} identified as CLVM={} via VirtualMachineTO pool type: {}", + boolean isClvm = StoragePoolType.CLVM == primaryStore.getPoolType() || + StoragePoolType.CLVM_NG == primaryStore.getPoolType(); + LOGGER.debug("Disk {} identified as CLVM/CLVM_NG={} via VirtualMachineTO pool type: {}", diskPath, isClvm, primaryStore.getPoolType()); return isClvm; } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java index f2af46d4cc8..afaa19be2dd 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtResizeVolumeCommandWrapper.java @@ -113,7 +113,8 @@ public final class LibvirtResizeVolumeCommandWrapper extends CommandWrapper, String> prepareStorageClient(StoragePoolType type, String uuid, Map details) { StorageAdaptor adaptor = getStorageAdaptor(type); return adaptor.prepareStorageClient(uuid, details); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java index 2e30c972caf..80b81b7588e 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java @@ -344,15 +344,28 @@ public class KVMStorageProcessor implements StorageProcessor { path = destTempl.getUuid(); } - if (path != null && !storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details)) { - logger.warn("Failed to connect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); - return new PrimaryStorageDownloadAnswer("Failed to spool template disk at path: " + path + ", in storage pool id: " + primaryStore.getUuid()); - } + if (primaryPool.getType() == StoragePoolType.CLVM_NG) { + logger.info("Copying template {} to CLVM_NG pool {}", + destTempl.getUuid(), primaryPool.getUuid()); - primaryVol = storagePoolMgr.copyPhysicalDisk(tmplVol, path != null ? path : destTempl.getUuid(), primaryPool, cmd.getWaitInMillSeconds()); + try { + storagePoolMgr.createTemplateOnClvmNg(tmplVol.getPath(), path, cmd.getWaitInMillSeconds(), primaryPool); + primaryVol = primaryPool.getPhysicalDisk("template-" + path); + } catch (Exception e) { + logger.error("Failed to create CLVM_NG template: {}", e.getMessage(), e); + return new PrimaryStorageDownloadAnswer("Failed to create CLVM_NG template: " + e.getMessage()); + } + } else { + if (path != null && !storagePoolMgr.connectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path, details)) { + logger.warn("Failed to connect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); + return new PrimaryStorageDownloadAnswer("Failed to spool template disk at path: " + path + ", in storage pool id: " + primaryStore.getUuid()); + } - if (!storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path)) { - logger.warn("Failed to disconnect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); + primaryVol = storagePoolMgr.copyPhysicalDisk(tmplVol, path != null ? path : destTempl.getUuid(), primaryPool, cmd.getWaitInMillSeconds()); + + if (!storagePoolMgr.disconnectPhysicalDisk(primaryStore.getPoolType(), primaryStore.getUuid(), path)) { + logger.warn("Failed to disconnect physical disk at path: {}, in storage pool [id: {}, name: {}]", path, primaryStore.getUuid(), primaryStore.getName()); + } } } else { primaryVol = storagePoolMgr.copyPhysicalDisk(tmplVol, UUID.randomUUID().toString(), primaryPool, cmd.getWaitInMillSeconds()); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java index 6c65a014395..cb936dc7f50 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java @@ -35,6 +35,8 @@ import java.util.stream.Collectors; import com.cloud.agent.properties.AgentProperties; import com.cloud.agent.properties.AgentPropertiesFileHandler; import com.cloud.utils.script.OutputInterpreter; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.utils.cryptsetup.KeyFile; import org.apache.cloudstack.utils.qemu.QemuImageOptions; @@ -48,6 +50,7 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.joda.time.Duration; import org.libvirt.Connect; import org.libvirt.LibvirtException; import org.libvirt.Secret; @@ -668,10 +671,10 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { try { StorageVol vol = getVolume(libvirtPool.getPool(), volumeUuid); - // Check if volume was found - if null, treat as not found and trigger fallback for CLVM + // Check if volume was found - if null, treat as not found and trigger fallback for CLVM/CLVM_NG if (vol == null) { - logger.debug("Volume " + volumeUuid + " not found in libvirt, will check for CLVM fallback"); - if (pool.getType() == StoragePoolType.CLVM) { + logger.debug("Volume " + volumeUuid + " not found in libvirt, will check for CLVM/CLVM_NG fallback"); + if (pool.getType() == StoragePoolType.CLVM || pool.getType() == StoragePoolType.CLVM_NG) { return getPhysicalDisk(volumeUuid, pool, libvirtPool); } @@ -709,8 +712,8 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { return disk; } catch (LibvirtException e) { logger.debug("Failed to get volume from libvirt: " + e.getMessage()); - // For CLVM, try direct block device access as fallback - if (pool.getType() == StoragePoolType.CLVM) { + // For CLVM/CLVM_NG, try direct block device access as fallback + if (pool.getType() == StoragePoolType.CLVM || pool.getType() == StoragePoolType.CLVM_NG) { return getPhysicalDisk(volumeUuid, pool, libvirtPool); } @@ -745,7 +748,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { return getPhysicalDiskViaDirectBlockDevice(volumeUuid, pool); } - private String getVgName(KVMStoragePool pool, String sourceDir) { + private String getVgName(String sourceDir) { String vgName = sourceDir; if (vgName.startsWith("/")) { String[] parts = vgName.split("/"); @@ -771,7 +774,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { if (sourceDir == null || sourceDir.isEmpty()) { throw new CloudRuntimeException("CLVM pool sourceDir is not set, cannot determine VG name"); } - String vgName = getVgName(pool, sourceDir); + String vgName = getVgName(sourceDir); logger.debug("Using VG name: {} (from sourceDir: {}) ", vgName, sourceDir); // Check if the LV exists in LVM using lvs command @@ -841,12 +844,23 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { } KVMPhysicalDisk disk = new KVMPhysicalDisk(lvPath, volumeUuid, pool); - disk.setFormat(PhysicalDiskFormat.RAW); + + // Detect correct format based on pool type + PhysicalDiskFormat diskFormat = PhysicalDiskFormat.RAW; // Default for legacy CLVM + if (pool.getType() == StoragePoolType.CLVM_NG) { + // CLVM_NG uses QCOW2 format on LVM block devices + diskFormat = PhysicalDiskFormat.QCOW2; + logger.debug("CLVM_NG pool detected, setting disk format to QCOW2 for volume {}", volumeUuid); + } else { + logger.debug("CLVM pool detected, setting disk format to RAW for volume {}", volumeUuid); + } + + disk.setFormat(diskFormat); disk.setSize(size); disk.setVirtualSize(size); - logger.info("Successfully accessed CLVM volume via direct block device: {} " + - "with size: {} bytes",lvPath, size); + logger.info("Successfully accessed CLVM/CLVM_NG volume via direct block device: {} " + + "with format: {} and size: {} bytes", lvPath, diskFormat, size); return disk; @@ -982,7 +996,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { try { sp = createNetfsStoragePool(PoolType.GLUSTERFS, conn, name, host, path, null); } catch (LibvirtException e) { - logger.error("Failed to create glusterfs mount: " + host + ":" + path , e); + logger.error("Failed to create glusterlvm_fs mount: " + host + ":" + path , e); logger.error(e.getStackTrace()); throw new CloudRuntimeException(e.toString()); } @@ -990,7 +1004,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { sp = createSharedStoragePool(conn, name, host, path); } else if (type == StoragePoolType.RBD) { sp = createRBDStoragePool(conn, name, host, port, userInfo, path); - } else if (type == StoragePoolType.CLVM) { + } else if (type == StoragePoolType.CLVM || type == StoragePoolType.CLVM_NG) { sp = createCLVMStoragePool(conn, name, host, path); } } @@ -1276,14 +1290,14 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { @Override public boolean connectPhysicalDisk(String name, KVMStoragePool pool, Map details, boolean isVMMigrate) { // this is for managed storage that needs to prep disks prior to use - if (pool.getType() == StoragePoolType.CLVM && isVMMigrate) { - logger.info("Activating CLVM volume {} at location: {} in shared mode for VM migration", name, pool.getLocalPath() + File.separator + name); + if ((pool.getType() == StoragePoolType.CLVM || pool.getType() == StoragePoolType.CLVM_NG) && isVMMigrate) { + logger.info("Activating CLVM/CLVM_NG volume {} at location: {} in shared mode for VM migration", name, pool.getLocalPath() + File.separator + name); Script activateVolInSharedMode = new Script("lvchange", 5000, logger); activateVolInSharedMode.add("-asy"); activateVolInSharedMode.add(pool.getLocalPath() + File.separator + name); String result = activateVolInSharedMode.execute(); if (result != null) { - logger.error("Failed to activate CLVM volume {} in shared mode for VM migration. Command output: {}", name, result); + logger.error("Failed to activate CLVM/CLVM_NG volume {} in shared mode for VM migration. Command output: {}", name, result); return false; } } @@ -1395,9 +1409,9 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { } } - // For CLVM pools, always use direct LVM cleanup to ensure secure zero-fill - if (pool.getType() == StoragePoolType.CLVM) { - logger.info("CLVM pool detected - using direct LVM cleanup with secure zero-fill for volume {}", uuid); + // For CLVM/CLVM_NG pools, always use direct LVM cleanup to ensure secure zero-fill + if (pool.getType() == StoragePoolType.CLVM || pool.getType() == StoragePoolType.CLVM_NG) { + logger.info("CLVM/CLVM_NG pool detected - using direct LVM cleanup with secure zero-fill for volume {}", uuid); return cleanupCLVMVolume(uuid, pool); } @@ -1441,7 +1455,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { logger.debug("Source directory is null or empty, cannot determine VG name for CLVM pool {}, skipping direct cleanup", pool.getUuid()); return true; } - String vgName = getVgName(pool, sourceDir); + String vgName = getVgName(sourceDir); logger.info("Determined VG name: {} for pool: {}", vgName, pool.getUuid()); if (vgName == null || vgName.isEmpty()) { @@ -1538,6 +1552,14 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { Script.runSimpleBashScript("chmod 755 " + disk.getPath()); Script.runSimpleBashScript("tar -x -f " + template.getPath() + "/*.tar -C " + disk.getPath(), timeout); } else if (format == PhysicalDiskFormat.QCOW2) { + if (destPool.getType() == StoragePoolType.CLVM_NG) { + logger.info("Creating CLVM_NG volume {} with backing file from template {}", newUuid, template.getName()); + String backingFile = getClvmBackingFile(template, destPool); + + disk = createClvmNgDiskWithBacking(newUuid, timeout, size, backingFile, destPool); + return disk; + } + QemuImg qemu = new QemuImg(timeout); QemuImgFile destFile = new QemuImgFile(disk.getPath(), format); if (size > template.getVirtualSize()) { @@ -1600,6 +1622,92 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { return disk; } + private String getClvmBackingFile(KVMPhysicalDisk template, KVMStoragePool destPool) { + String templateLvName = "template-" + template.getName(); + KVMPhysicalDisk templateOnPrimary = null; + + try { + templateOnPrimary = destPool.getPhysicalDisk(templateLvName); + } catch (CloudRuntimeException e) { + logger.warn("Template {} not found on CLVM_NG pool {}.", templateLvName, destPool.getUuid()); + } + + String backingFile; + if (templateOnPrimary != null) { + backingFile = templateOnPrimary.getPath(); + logger.info("Using template on primary storage as backing file: {}", backingFile); + + ensureTemplateLvInSharedMode(backingFile); + } else { + logger.error("Template {} should be on primary storage before creating volumes from it", templateLvName); + throw new CloudRuntimeException(String.format("Template not found on CLVM_NG primary storage: {}." + + "Template must be copied to primary storage first.", templateLvName)); + } + return backingFile; + } + + /** + * Ensures a template LV is activated in shared mode so multiple VMs can use it as a backing file. + * + * @param templatePath The full path to the template LV (e.g., /dev/vgname/template-uuid) + * @param throwOnFailure If true, throws CloudRuntimeException on failure; if false, logs warning and continues + */ + private void ensureTemplateLvInSharedMode(String templatePath, boolean throwOnFailure) { + try { + Script checkLvs = new Script("lvs", Duration.millis(5000), logger); + checkLvs.add("--noheadings"); + checkLvs.add("-o", "lv_attr"); + checkLvs.add(templatePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = checkLvs.execute(parser); + + if (result == null && parser.getLines() != null && !parser.getLines().isEmpty()) { + String lvAttr = parser.getLines().trim(); + if (lvAttr.length() >= 6) { + char activeChar = lvAttr.charAt(4); // 'a' = active, '-' = inactive + char sharedChar = lvAttr.charAt(5); // 's' = shared, 'e' = exclusive, '-' = not set + + if (activeChar != 'a' || sharedChar != 's') { + logger.info("Template LV {} is not in shared mode (attr: {}). Activating in shared mode.", + templatePath, lvAttr); + + Script lvchange = new Script("lvchange", Duration.millis(5000), logger); + lvchange.add("-asy"); + lvchange.add(templatePath); + result = lvchange.execute(); + + if (result != null) { + String errorMsg = "Failed to activate template LV " + templatePath + " in shared mode: " + result; + if (throwOnFailure) { + throw new CloudRuntimeException(errorMsg); + } else { + logger.warn(errorMsg); + } + } else { + logger.info("Successfully activated template LV {} in shared mode", templatePath); + } + } else { + logger.debug("Template LV {} is already in shared mode", templatePath); + } + } + } + } catch (CloudRuntimeException e) { + throw e; + } catch (Exception e) { + String errorMsg = "Failed to check/ensure template LV shared mode for " + templatePath + ": " + e.getMessage(); + if (throwOnFailure) { + throw new CloudRuntimeException(errorMsg, e); + } else { + logger.warn(errorMsg, e); + } + } + } + + private void ensureTemplateLvInSharedMode(String templatePath) { + ensureTemplateLvInSharedMode(templatePath, false); + } + private KVMPhysicalDisk createDiskFromTemplateOnRBD(KVMPhysicalDisk template, String name, PhysicalDiskFormat format, Storage.ProvisioningType provisioningType, long size, KVMStoragePool destPool, int timeout){ @@ -2071,4 +2179,197 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { private void deleteDirVol(LibvirtStoragePool pool, StorageVol vol) throws LibvirtException { Script.runSimpleBashScript("rm -r --interactive=never " + vol.getPath()); } + + /** + * Get Physical Extent (PE) from the volume group + * @param vgName Volume group name + * @return PE size in bytes, defauts to 4MiB if it cannot be determined + */ + private long getVgPhysicalExtentSize(String vgName) { + String warningMessage = String.format("Failed to get PE size for VG %s, defaulting to 4MiB", vgName); + try { + Script vgDisplay = new Script("vgdisplay", 300000, logger); + vgDisplay.add("--units", "b"); // Output in bytes + vgDisplay.add("-C"); // Columnar output + vgDisplay.add("--noheadings"); + vgDisplay.add("-o", "vg_extent_size"); + vgDisplay.add(vgName); + + String output = vgDisplay.execute(); + if (output != null) { + output = output.trim(); + if (output.endsWith("B")) { + output = output.substring(0, output.length() - 1); + } + logger.debug("Physical Extent size for VG {} is {} bytes", vgName, output); + return Long.parseLong(output); + } else { + logger.warn(warningMessage); + } + } catch (Exception e) { + logger.warn(warningMessage, e.getMessage()); + } + final long DEFAULT_PE_SIZE = 4 * 1024 * 1024L; + logger.info("Using default PE size for VG {}: {} bytes (4 MiB)", vgName, DEFAULT_PE_SIZE); + return DEFAULT_PE_SIZE; + } + + /** + * Calculate LVM LV size for QCOW2 image accounting for metadata overhead + * @param qcow2PhysicalSize Physical size in bytes from qemu-img info + * @param vgName Volume group name to query PE size + * @return Size in bytes to allocate for LV + */ + private long calculateClvmNgLvSize(long qcow2PhysicalSize, String vgName) { + long peSize = getVgPhysicalExtentSize(vgName); + long roundedSize = ((qcow2PhysicalSize + peSize - 1) / peSize) * peSize; + + long finalSize = roundedSize + peSize; + logger.info("Calculated LV size for QCOW2 physical size {} bytes: {} bytes " + + "(rounded to {} PEs + 1 PE overhead, PE size = {} bytes)", + qcow2PhysicalSize, finalSize, + roundedSize / peSize, peSize); + + return finalSize; + } + + + /** + * Get physical size of QCOW2 image + */ + private long getQcow2PhysicalSize(String imagePath) { + Script qemuImg = new Script("qemu-img", 300000, logger); + qemuImg.add("info"); + qemuImg.add("--output=json"); + qemuImg.add(imagePath); + String output = qemuImg.execute(); + + JsonObject info = JsonParser.parseString(output).getAsJsonObject(); + return info.get("actual-size").getAsLong(); + } + + private KVMPhysicalDisk createClvmNgDiskWithBacking(String volumeUuid, int timeout, long virtualSize, String backingFile, KVMStoragePool pool) { + String vgName = getVgName(pool.getLocalPath()); + long lvSize = calculateClvmNgLvSize(virtualSize, vgName); + String volumePath = "/dev/" + vgName + "/" + volumeUuid; + + logger.debug("Creating CLVM_NG volume {} with LV size {} bytes (virtual size: {} bytes)", volumeUuid, lvSize, virtualSize); + + Script lvcreate = new Script("lvcreate", Duration.millis(timeout), logger); + lvcreate.add("-n", volumeUuid); + lvcreate.add("-L", lvSize + "B"); + lvcreate.add(vgName); + + String result = lvcreate.execute(); + if (result != null) { + throw new CloudRuntimeException("Failed to create LV for CLVM_NG volume: " + result); + } + + Script qemuImg = new Script("qemu-img", Duration.millis(timeout), logger); + qemuImg.add("create"); + qemuImg.add("-f", "qcow2"); + + StringBuilder qcow2Options = new StringBuilder(); + qcow2Options.append("preallocation=metadata"); + qcow2Options.append(",extended_l2=on"); + qcow2Options.append(",cluster_size=128k"); + + if (backingFile != null && !backingFile.isEmpty()) { + qcow2Options.append(",backing_file=").append(backingFile); + qcow2Options.append(",backing_fmt=qcow2"); + logger.debug("Creating CLVM_NG volume with backing file: {}", backingFile); + } + + qemuImg.add("-o", qcow2Options.toString()); + qemuImg.add(volumePath); + qemuImg.add(virtualSize + ""); + + result = qemuImg.execute(); + if (result != null) { + removeLvOnFailure(volumePath, timeout); + throw new CloudRuntimeException("Failed to create QCOW2 on CLVM_NG volume: " + result); + } + + KVMPhysicalDisk disk = new KVMPhysicalDisk(volumePath, volumeUuid, pool); + disk.setFormat(PhysicalDiskFormat.QCOW2); + disk.setSize(lvSize); + disk.setVirtualSize(virtualSize); + + logger.info("Successfully created CLVM_NG volume {} with backing file (LV size: {}, virtual size: {})", + volumeUuid, lvSize, virtualSize); + + return disk; + } + + public void createTemplateOnClvmNg(String templatePath, String templateUuid, int timeout, KVMStoragePool pool) { + String vgName = getVgName(pool.getLocalPath()); + String lvName = "template-" + templateUuid; + String lvPath = "/dev/" + vgName + "/" + lvName; + + if (lvExists(lvPath)) { + logger.info("Template LV {} already exists in VG {}. Skipping creation.", lvName, vgName); + return; + } + + logger.info("Creating new template LV {} in VG {} for template {}", lvName, vgName, templateUuid); + + long physicalSize = getQcow2PhysicalSize(templatePath); + long lvSize = calculateClvmNgLvSize(physicalSize, vgName); + + Script lvcreate = new Script("lvcreate", Duration.millis(timeout), logger); + lvcreate.add("-n", lvName); + lvcreate.add("-L", lvSize + "B"); + lvcreate.add(vgName); + String result = lvcreate.execute(); + if (result != null) { + throw new CloudRuntimeException("Failed to create LV for CLVM_NG template: " + result); + } + + + Script qemuImgConvert = new Script("qemu-img", Duration.millis(timeout), logger); + qemuImgConvert.add("convert"); + qemuImgConvert.add(templatePath); + qemuImgConvert.add("-O", "qcow2"); + qemuImgConvert.add(lvPath); + result = qemuImgConvert.execute(); + + if (result != null) { + removeLvOnFailure(lvPath, timeout); + throw new CloudRuntimeException("Failed to convert template to CLVM_NG volume: " + result); + } + + logger.info("Created template LV {} with size {} bytes (physical: {}, overhead: {})", + lvName, lvSize, physicalSize, lvSize - physicalSize); + + try { + ensureTemplateLvInSharedMode(lvPath, true); + } catch (CloudRuntimeException e) { + logger.error("Failed to activate template LV {} in shared mode. Cleaning up.", lvPath); + removeLvOnFailure(lvPath, timeout); + throw e; + } + + KVMPhysicalDisk templateDisk = new KVMPhysicalDisk(lvPath, lvName, pool); + templateDisk.setFormat(PhysicalDiskFormat.QCOW2); + templateDisk.setVirtualSize(physicalSize); + templateDisk.setSize(lvSize); + + } + + private boolean lvExists(String lvPath) { + Script checkLv = new Script("lvs", Duration.millis(5000), logger); + checkLv.add("--noheadings"); + checkLv.add("--unbuffered"); + checkLv.add(lvPath); + String checkResult = checkLv.execute(); + return checkResult == null; + } + + private void removeLvOnFailure(String lvPath, int timeout) { + Script lvremove = new Script("lvremove", Duration.millis(timeout), logger); + lvremove.add("-f"); + lvremove.add(lvPath); + lvremove.execute(); + } + } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java index ab39f7bc6ff..83898ed35c3 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStoragePool.java @@ -212,7 +212,7 @@ public class LibvirtStoragePool implements KVMStoragePool { @Override public boolean isExternalSnapshot() { - if (this.type == StoragePoolType.CLVM || type == StoragePoolType.RBD) { + if (this.type == StoragePoolType.CLVM || this.type == StoragePoolType.CLVM_NG || type == StoragePoolType.RBD) { return true; } return false; @@ -277,6 +277,10 @@ public class LibvirtStoragePool implements KVMStoragePool { return this.type; } + public void setType(StoragePoolType type) { + this.type = type; + } + public StoragePool getPool() { return this._pool; } diff --git a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java index e4eb10f51fb..cf9aa16c7f1 100644 --- a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java +++ b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java @@ -233,6 +233,11 @@ public class CloudStackPrimaryDataStoreLifeCycleImpl extends BasePrimaryDataStor parameters.setHost(storageHost); parameters.setPort(0); parameters.setPath(hostPath.replaceFirst("/", "")); + } else if (scheme.equalsIgnoreCase("clvm_ng")) { + parameters.setType(StoragePoolType.CLVM_NG); + parameters.setHost(storageHost); + parameters.setPort(0); + parameters.setPath(hostPath.replaceFirst("/", "")); } else if (scheme.equalsIgnoreCase("rbd")) { if (port == -1) { port = 0; diff --git a/server/src/main/java/com/cloud/storage/ClvmLockManager.java b/server/src/main/java/com/cloud/storage/ClvmLockManager.java index 0c9a4fd4de1..90e5839ea62 100644 --- a/server/src/main/java/com/cloud/storage/ClvmLockManager.java +++ b/server/src/main/java/com/cloud/storage/ClvmLockManager.java @@ -18,6 +18,7 @@ */ package com.cloud.storage; +import java.util.Arrays; import javax.inject.Inject; import com.cloud.agent.AgentManager; @@ -46,6 +47,10 @@ public class ClvmLockManager { protected Logger logger = LogManager.getLogger(getClass()); + public static boolean isClvmPoolType(Storage.StoragePoolType poolType) { + return Arrays.asList(Storage.StoragePoolType.CLVM, Storage.StoragePoolType.CLVM_NG).contains(poolType); + } + public Long getClvmLockHostId(Long volumeId, String volumeUuid) { VolumeDetailVO detail = _volsDetailsDao.findDetail(volumeId, VolumeInfo.CLVM_LOCK_HOST_ID); if (detail != null && detail.getValue() != null && !detail.getValue().isEmpty()) { diff --git a/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java b/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java index 5c90b5cbee6..20b6a8868ba 100644 --- a/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java @@ -436,6 +436,12 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme vmSnapshotType = VMSnapshot.Type.Disk; } + // CLVM_NG: Block VM snapshots until Phase 2 implementation is complete + if (rootVolumePool.getPoolType() == Storage.StoragePoolType.CLVM_NG) { + throw new InvalidParameterValueException("VM snapshots are not yet supported on CLVM_NG storage pools. " + + "This feature will be available in a future release."); + } + try { return createAndPersistVMSnapshot(userVmVo, vsDescription, vmSnapshotName, vsDisplayName, vmSnapshotType); } catch (Exception e) { diff --git a/ui/src/views/infra/AddPrimaryStorage.vue b/ui/src/views/infra/AddPrimaryStorage.vue index d46396bbb3a..4d7d2f64663 100644 --- a/ui/src/views/infra/AddPrimaryStorage.vue +++ b/ui/src/views/infra/AddPrimaryStorage.vue @@ -383,7 +383,7 @@ -
+
@@ -607,7 +607,7 @@ export default { const cluster = this.clusters.find(cluster => cluster.id === this.form.cluster) this.hypervisorType = cluster.hypervisortype if (this.hypervisorType === 'KVM') { - this.protocols = ['nfs', 'SharedMountPoint', 'RBD', 'CLVM', 'Gluster', 'Linstor', 'custom', 'FiberChannel'] + this.protocols = ['nfs', 'SharedMountPoint', 'RBD', 'CLVM', 'CLVM_NG', 'Gluster', 'Linstor', 'custom', 'FiberChannel'] if (this.form.scope === 'host') { this.protocols.push('Filesystem') } @@ -729,6 +729,15 @@ export default { } return url }, + clvmNgURL (vgname) { + var url + if (vgname.indexOf('://') === -1) { + url = 'clvm_ng://localhost/' + vgname + } else { + url = vgname + } + return url + }, vmfsURL (server, path) { var url if (server.indexOf('://') === -1) { @@ -853,6 +862,9 @@ export default { } else if (values.protocol === 'CLVM') { var vg = (values.volumegroup.substring(0, 1) !== '/') ? ('/' + values.volumegroup) : values.volumegroup url = this.clvmURL(vg) + } else if (values.protocol === 'CLVM_NG') { + var vg = (values.volumegroup.substring(0, 1) !== '/') ? ('/' + values.volumegroup) : values.volumegroup + url = this.clvmNgURL(vg) } else if (values.protocol === 'RBD') { url = this.rbdURL(values.radosmonitor, values.radospool, values.radosuser, values.radossecret) if (values.datapool) {