From cc924c5b3ac5fb5944ec95c9ffd31a6c1383489e Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Fri, 13 Mar 2026 17:12:15 -0400 Subject: [PATCH] Add support for clvm_ng - which allows qcow2 on block storage , linked clones, etc --- .../resource/LibvirtComputingResource.java | 23 +- .../LibvirtResizeVolumeCommandWrapper.java | 2 +- .../kvm/storage/KVMStorageProcessor.java | 135 +-- .../kvm/storage/LibvirtStorageAdaptor.java | 788 +++++++++++++----- scripts/storage/qcow2/managesnapshot.sh | 49 +- .../com/cloud/storage/StorageManagerImpl.java | 5 + .../storage/snapshot/SnapshotManagerImpl.java | 2 +- ui/src/views/infra/AddPrimaryStorage.vue | 2 +- .../main/java/com/cloud/utils/UriUtils.java | 33 + 9 files changed, 754 insertions(+), 285 deletions(-) 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 fbe48d4318c..0a90352f70d 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 @@ -3682,7 +3682,6 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv 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 || 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 { @@ -6585,7 +6584,7 @@ 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); + LOGGER.info("{} for volume [{}]", logMessage, volumePath); Script cmd = new Script("lvchange", Duration.standardSeconds(300), LOGGER); cmd.add(lvchangeFlag); @@ -6594,19 +6593,19 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv String result = cmd.execute(); if (result != null) { String errorMsg = String.format( - "[CLVM Migration] Failed to set volume [%s] to %s state. Command result: %s", + "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.", + LOGGER.info("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", + "Exception while setting volume [%s] to %s state: %s", volumePath, stateDescription, e.getMessage()); LOGGER.error(errorMsg, e); throw new CloudRuntimeException(errorMsg, e); @@ -6616,19 +6615,29 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv public static void activateClvmVolumeExclusive(String volumePath) { modifyClvmVolumeState(volumePath, ClvmVolumeState.EXCLUSIVE.getLvchangeFlag(), ClvmVolumeState.EXCLUSIVE.getDescription(), - "Activating CLVM volume in exclusive mode for copy"); + "Activating CLVM volume in exclusive mode"); } public static void deactivateClvmVolume(String volumePath) { try { modifyClvmVolumeState(volumePath, ClvmVolumeState.DEACTIVATE.getLvchangeFlag(), ClvmVolumeState.DEACTIVATE.getDescription(), - "Deactivating CLVM volume after copy"); + "Deactivating CLVM volume"); } catch (Exception e) { LOGGER.warn("Failed to deactivate CLVM volume {}: {}", volumePath, e.getMessage()); } } + public static void setClvmVolumeToSharedMode(String volumePath) { + try { + modifyClvmVolumeState(volumePath, ClvmVolumeState.SHARED.getLvchangeFlag(), + ClvmVolumeState.SHARED.getDescription(), + "Setting CLVM volume to shared mode"); + } catch (Exception e) { + LOGGER.warn("Failed to set CLVM volume {} to shared mode: {}", 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. 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 afaa19be2dd..a43b584dd6d 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,7 @@ public final class LibvirtResizeVolumeCommandWrapper extends CommandWrapper details) { + String volgroupName = path.replaceFirst("^/", ""); + String volgroupPath = "/dev/" + volgroupName; + + logger.info("Creating virtual CLVM/CLVM_NG pool {} without libvirt using direct LVM access", volgroupName); + + long[] vgStats = getVgStats(volgroupName); + + LibvirtStoragePool pool = new LibvirtStoragePool(uuid, volgroupName, type, this, null); + pool.setLocalPath(volgroupPath); + setPoolCapacityFromVgStats(pool, vgStats, volgroupName); + + if (details != null) { + pool.setDetails(details); + } + + return pool; + } + + private List getNFSMountOptsFromDetails(StoragePoolType type, Map details) { List nfsMountOpts = null; if (!type.equals(StoragePoolType.NetworkFilesystem) || details == null) { @@ -587,14 +700,11 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { Connect conn = LibvirtConnection.getConnection(); storage = conn.storagePoolLookupByUUIDString(uuid); - if (storage.getInfo().state != StoragePoolState.VIR_STORAGE_POOL_RUNNING) { - logger.warn("Storage pool " + uuid + " is not in running state. Attempting to start it."); - storage.create(0); - } LibvirtStoragePoolDef spd = getStoragePoolDef(conn, storage); if (spd == null) { throw new CloudRuntimeException("Unable to parse the storage pool definition for storage pool " + uuid); } + StoragePoolType type = null; if (spd.getPoolType() == LibvirtStoragePoolDef.PoolType.NETFS) { type = StoragePoolType.NetworkFilesystem; @@ -610,6 +720,12 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { type = StoragePoolType.PowerFlex; } + // Skip pool activation for CLVM/CLVM_NG - we keep them inactive and use direct LVM commands + if (storage.getInfo().state != StoragePoolState.VIR_STORAGE_POOL_RUNNING && type != StoragePoolType.CLVM && type != StoragePoolType.CLVM_NG) { + logger.warn("Storage pool " + uuid + " is not in running state. Attempting to start it."); + storage.create(0); + } + LibvirtStoragePool pool = new LibvirtStoragePool(uuid, storage.getName(), type, this, storage); if (pool.getType() != StoragePoolType.RBD && pool.getType() != StoragePoolType.PowerFlex) @@ -647,15 +763,31 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { logger.info("Asking libvirt to refresh storage pool " + uuid); pool.refresh(); } - pool.setCapacity(storage.getInfo().capacity); - pool.setUsed(storage.getInfo().allocation); - updateLocalPoolIops(pool); - pool.setAvailable(storage.getInfo().available); - logger.debug("Successfully refreshed pool " + uuid + - " Capacity: " + toHumanReadableSize(storage.getInfo().capacity) + - " Used: " + toHumanReadableSize(storage.getInfo().allocation) + - " Available: " + toHumanReadableSize(storage.getInfo().available)); + if (type == StoragePoolType.CLVM || type == StoragePoolType.CLVM_NG) { + logger.debug("Getting capacity for CLVM/CLVM_NG pool {} using direct LVM commands", uuid); + String vgName = storage.getName(); + try { + long[] vgStats = getVgStats(vgName); + setPoolCapacityFromVgStats(pool, vgStats, vgName); + } catch (CloudRuntimeException e) { + logger.warn("Failed to get VG stats for CLVM/CLVM_NG pool {}: {}. Using libvirt values (may be 0)", vgName, e.getMessage()); + pool.setCapacity(storage.getInfo().capacity); + pool.setUsed(storage.getInfo().allocation); + pool.setAvailable(storage.getInfo().available); + } + } else { + pool.setCapacity(storage.getInfo().capacity); + pool.setUsed(storage.getInfo().allocation); + pool.setAvailable(storage.getInfo().available); + + logger.debug("Successfully refreshed pool {} Capacity: {} Used: {} Available: {}", + uuid, toHumanReadableSize(storage.getInfo().capacity), + toHumanReadableSize(storage.getInfo().allocation), + toHumanReadableSize(storage.getInfo().available)); + } + + updateLocalPoolIops(pool); return pool; } catch (LibvirtException e) { @@ -667,15 +799,20 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { @Override public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) { LibvirtStoragePool libvirtPool = (LibvirtStoragePool)pool; + boolean isClvmPool = (pool.getType() == StoragePoolType.CLVM || pool.getType() == StoragePoolType.CLVM_NG); + + // For CLVM pools without libvirt backing, use direct block device access immediately + if (isClvmPool && libvirtPool.getPool() == null) { + logger.debug("CLVM/CLVM_NG pool has no libvirt backing, using direct block device access for volume: {}", volumeUuid); + return getPhysicalDiskViaDirectBlockDevice(volumeUuid, pool); + } try { StorageVol vol = getVolume(libvirtPool.getPool(), volumeUuid); - - // 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/CLVM_NG fallback"); - if (pool.getType() == StoragePoolType.CLVM || pool.getType() == StoragePoolType.CLVM_NG) { - return getPhysicalDisk(volumeUuid, pool, libvirtPool); + if (isClvmPool) { + return getPhysicalDiskWithClvmFallback(volumeUuid, pool, libvirtPool); } throw new CloudRuntimeException("Volume " + volumeUuid + " not found in libvirt pool"); @@ -712,17 +849,20 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { return disk; } catch (LibvirtException e) { logger.debug("Failed to get volume from libvirt: " + e.getMessage()); - // 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); + if (isClvmPool) { + return getPhysicalDiskWithClvmFallback(volumeUuid, pool, libvirtPool); } throw new CloudRuntimeException(e.toString()); } } - private KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool, LibvirtStoragePool libvirtPool) { - logger.info("CLVM volume not visible to libvirt, attempting direct block device access for volume: {}", volumeUuid); + /** + * CLVM fallback: First tries to refresh libvirt pool to make volume visible, + * if that fails, accesses volume directly via block device path. + */ + private KVMPhysicalDisk getPhysicalDiskWithClvmFallback(String volumeUuid, KVMStoragePool pool, LibvirtStoragePool libvirtPool) { + logger.info("CLVM volume not visible to libvirt, attempting pool refresh for volume: {}", volumeUuid); try { logger.debug("Refreshing libvirt storage pool: {}", pool.getUuid()); @@ -744,7 +884,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { logger.debug("Pool refresh failed or volume still not found: {}", refreshEx.getMessage()); } - // Still not found after refresh, try direct block device access + logger.info("Falling back to direct block device access for volume: {}", volumeUuid); return getPhysicalDiskViaDirectBlockDevice(volumeUuid, pool); } @@ -768,107 +908,185 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { */ private KVMPhysicalDisk getPhysicalDiskViaDirectBlockDevice(String volumeUuid, KVMStoragePool pool) { try { - // For CLVM, pool sourceDir contains the VG path (e.g., "/dev/acsvg") - // Extract the VG name - String sourceDir = pool.getLocalPath(); - if (sourceDir == null || sourceDir.isEmpty()) { - throw new CloudRuntimeException("CLVM pool sourceDir is not set, cannot determine VG name"); - } - String vgName = getVgName(sourceDir); - logger.debug("Using VG name: {} (from sourceDir: {}) ", vgName, sourceDir); + String vgName = extractVgNameFromPool(pool); + verifyLvExistsInVg(volumeUuid, vgName); - // Check if the LV exists in LVM using lvs command - logger.debug("Checking if volume {} exists in VG {}", volumeUuid, vgName); - Script checkLvCmd = new Script("/usr/sbin/lvs", 5000, logger); - checkLvCmd.add("--noheadings"); - checkLvCmd.add("--unbuffered"); - checkLvCmd.add(vgName + "/" + volumeUuid); + logger.info("Volume {} exists in LVM but not visible to libvirt, accessing directly", volumeUuid); - String checkResult = checkLvCmd.execute(); - if (checkResult != null) { - logger.debug("Volume {} does not exist in VG {}: {}", volumeUuid, vgName, checkResult); - throw new CloudRuntimeException(String.format("Storage volume not found: no storage vol with matching name '%s'", volumeUuid)); - } + String lvPath = findAccessibleDeviceNode(volumeUuid, vgName, pool); + long size = getClvmVolumeSize(lvPath); - logger.info("Volume {} exists in LVM but not visible to libvirt, accessing directly", volumeUuid); + KVMPhysicalDisk disk = createPhysicalDiskFromClvmLv(lvPath, volumeUuid, pool, size); - // Try standard device path first - String lvPath = "/dev/" + vgName + "/" + volumeUuid; - File lvDevice = new File(lvPath); - - if (!lvDevice.exists()) { - // Try device-mapper path with escaped hyphens - String vgNameEscaped = vgName.replace("-", "--"); - String volumeUuidEscaped = volumeUuid.replace("-", "--"); - lvPath = "/dev/mapper/" + vgNameEscaped + "-" + volumeUuidEscaped; - lvDevice = new File(lvPath); - - if (!lvDevice.exists()) { - logger.warn("Volume exists in LVM but device node not found: {}", volumeUuid); - throw new CloudRuntimeException(String.format("Could not find volume %s " + - "in VG %s - volume exists in LVM but device node not accessible", volumeUuid, vgName)); - } - } - - long size = 0; - try { - Script lvsCmd = new Script("/usr/sbin/lvs", 5000, logger); - lvsCmd.add("--noheadings"); - lvsCmd.add("--units"); - lvsCmd.add("b"); - lvsCmd.add("-o"); - lvsCmd.add("lv_size"); - lvsCmd.add(lvPath); - - OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); - String result = lvsCmd.execute(parser); - - String output = null; - if (result == null) { - output = parser.getLines(); - } else { - output = result; - } - - if (output != null && !output.isEmpty()) { - String sizeStr = output.trim().replaceAll("[^0-9]", ""); - if (!sizeStr.isEmpty()) { - size = Long.parseLong(sizeStr); - } - } - } catch (Exception sizeEx) { - logger.warn("Failed to get size for CLVM volume via lvs: {}", sizeEx.getMessage()); - if (lvDevice.isFile()) { - size = lvDevice.length(); - } - } - - KVMPhysicalDisk disk = new KVMPhysicalDisk(lvPath, volumeUuid, pool); - - // 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/CLVM_NG volume via direct block device: {} " + - "with format: {} and size: {} bytes", lvPath, diskFormat, size); + ensureTemplateAccessibility(volumeUuid, lvPath, pool); return disk; } catch (CloudRuntimeException ex) { throw ex; } catch (Exception ex) { - logger.error("Failed to access CLVM volume via direct block device: {}",volumeUuid, ex); - throw new CloudRuntimeException(String.format("Could not find volume %s: %s ",volumeUuid, ex.getMessage())); + logger.error("Failed to access CLVM volume via direct block device: {}", volumeUuid, ex); + throw new CloudRuntimeException(String.format("Could not find volume %s: %s ", volumeUuid, ex.getMessage())); + } + } + + private String extractVgNameFromPool(KVMStoragePool pool) { + String sourceDir = pool.getLocalPath(); + if (sourceDir == null || sourceDir.isEmpty()) { + throw new CloudRuntimeException("CLVM pool sourceDir is not set, cannot determine VG name"); + } + String vgName = getVgName(sourceDir); + logger.debug("Using VG name: {} (from sourceDir: {})", vgName, sourceDir); + return vgName; + } + + private void verifyLvExistsInVg(String volumeUuid, String vgName) { + logger.debug("Checking if volume {} exists in VG {}", volumeUuid, vgName); + Script checkLvCmd = new Script("/usr/sbin/lvs", 5000, logger); + checkLvCmd.add("--noheadings"); + checkLvCmd.add("--unbuffered"); + checkLvCmd.add(vgName + "/" + volumeUuid); + + String checkResult = checkLvCmd.execute(); + if (checkResult != null) { + logger.debug("Volume {} does not exist in VG {}: {}", volumeUuid, vgName, checkResult); + throw new CloudRuntimeException(String.format("Storage volume not found: no storage vol with matching name '%s'", volumeUuid)); + } + } + + private String findAccessibleDeviceNode(String volumeUuid, String vgName, KVMStoragePool pool) { + String lvPath = "/dev/" + vgName + "/" + volumeUuid; + File lvDevice = new File(lvPath); + + if (!lvDevice.exists()) { + lvPath = tryDeviceMapperPath(volumeUuid, vgName, lvDevice); + + if (!lvDevice.exists()) { + lvPath = handleMissingDeviceNode(volumeUuid, vgName, pool); + } + } + + return lvPath; + } + + private String tryDeviceMapperPath(String volumeUuid, String vgName, File lvDevice) { + String vgNameEscaped = vgName.replace("-", "--"); + String volumeUuidEscaped = volumeUuid.replace("-", "--"); + String mapperPath = "/dev/mapper/" + vgNameEscaped + "-" + volumeUuidEscaped; + File mapperDevice = new File(mapperPath); + + if (!mapperDevice.exists()) { + lvDevice = mapperDevice; + } + + return mapperPath; + } + + private String handleMissingDeviceNode(String volumeUuid, String vgName, KVMStoragePool pool) { + if (pool.getType() == StoragePoolType.CLVM_NG && volumeUuid.startsWith("template-")) { + return activateTemplateAndGetPath(volumeUuid, vgName); + } else { + logger.warn("Volume exists in LVM but device node not found: {}", volumeUuid); + throw new CloudRuntimeException(String.format("Could not find volume %s " + + "in VG %s - volume exists in LVM but device node not accessible", volumeUuid, vgName)); + } + } + + private String activateTemplateAndGetPath(String volumeUuid, String vgName) { + logger.info("Template volume {} device node not found. Attempting to activate in shared mode.", volumeUuid); + String templateLvPath = "/dev/" + vgName + "/" + volumeUuid; + + try { + ensureTemplateLvInSharedMode(templateLvPath, false); + + String lvPath = findDeviceNodeAfterActivation(templateLvPath, volumeUuid, vgName); + + logger.info("Successfully activated template volume {} at {}", volumeUuid, lvPath); + return lvPath; + + } catch (CloudRuntimeException e) { + throw new CloudRuntimeException(String.format("Failed to activate template volume %s " + + "in VG %s: %s", volumeUuid, vgName, e.getMessage()), e); + } + } + + private String findDeviceNodeAfterActivation(String templateLvPath, String volumeUuid, String vgName) { + File lvDevice = new File(templateLvPath); + String lvPath = templateLvPath; + + if (!lvDevice.exists()) { + String vgNameEscaped = vgName.replace("-", "--"); + String volumeUuidEscaped = volumeUuid.replace("-", "--"); + lvPath = "/dev/mapper/" + vgNameEscaped + "-" + volumeUuidEscaped; + lvDevice = new File(lvPath); + } + + if (!lvDevice.exists()) { + logger.error("Template volume {} still not accessible after activation attempt", volumeUuid); + throw new CloudRuntimeException(String.format("Could not activate template volume %s " + + "in VG %s - device node not accessible even after activation", volumeUuid, vgName)); + } + + return lvPath; + } + + private long getClvmVolumeSize(String lvPath) { + try { + Script lvsCmd = new Script("/usr/sbin/lvs", 5000, logger); + lvsCmd.add("--noheadings"); + lvsCmd.add("--units"); + lvsCmd.add("b"); + lvsCmd.add("-o"); + lvsCmd.add("lv_size"); + lvsCmd.add(lvPath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = lvsCmd.execute(parser); + + String output = (result == null) ? parser.getLines() : result; + + if (output != null && !output.isEmpty()) { + String sizeStr = output.trim().replaceAll("[^0-9]", ""); + if (!sizeStr.isEmpty()) { + return Long.parseLong(sizeStr); + } + } + } catch (Exception sizeEx) { + logger.warn("Failed to get size for CLVM volume via lvs: {}", sizeEx.getMessage()); + File lvDevice = new File(lvPath); + if (lvDevice.isFile()) { + return lvDevice.length(); + } + } + return 0; + } + + private KVMPhysicalDisk createPhysicalDiskFromClvmLv(String lvPath, String volumeUuid, + KVMStoragePool pool, long size) { + KVMPhysicalDisk disk = new KVMPhysicalDisk(lvPath, volumeUuid, pool); + + PhysicalDiskFormat diskFormat = (pool.getType() == StoragePoolType.CLVM_NG) + ? PhysicalDiskFormat.QCOW2 + : PhysicalDiskFormat.RAW; + + logger.debug("{} pool detected, setting disk format to {} for volume {}", + pool.getType(), diskFormat, volumeUuid); + + disk.setFormat(diskFormat); + disk.setSize(size); + disk.setVirtualSize(size); + + logger.info("Successfully accessed CLVM/CLVM_NG volume via direct block device: {} " + + "with format: {} and size: {} bytes", lvPath, diskFormat, size); + + return disk; + } + + private void ensureTemplateAccessibility(String volumeUuid, String lvPath, KVMStoragePool pool) { + if (pool.getType() == StoragePoolType.CLVM_NG && volumeUuid.startsWith("template-")) { + logger.info("Detected template volume {}. Ensuring it's activated in shared mode for backing file access.", + volumeUuid); + ensureTemplateLvInSharedMode(lvPath, false); } } @@ -988,7 +1206,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { try { sp = createNetfsStoragePool(PoolType.NETFS, conn, name, host, path, nfsMountOpts); } catch (LibvirtException e) { - logger.error("Failed to create netfs mount: " + host + ":" + path , e); + logger.error("Failed to create netfs mount: " + host + ":" + path, e); logger.error(e.getStackTrace()); throw new CloudRuntimeException(e.toString()); } @@ -996,7 +1214,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { try { sp = createNetfsStoragePool(PoolType.GLUSTERFS, conn, name, host, path, null); } catch (LibvirtException e) { - logger.error("Failed to create glusterlvm_fs mount: " + host + ":" + path , e); + logger.error("Failed to create glusterlvm_fs mount: " + host + ":" + path, e); logger.error(e.getStackTrace()); throw new CloudRuntimeException(e.toString()); } @@ -1006,6 +1224,10 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { sp = createRBDStoragePool(conn, name, host, port, userInfo, path); } else if (type == StoragePoolType.CLVM || type == StoragePoolType.CLVM_NG) { sp = createCLVMStoragePool(conn, name, host, path); + if (sp == null) { + logger.info("Falling back to virtual CLVM/CLVM_NG pool without libvirt for: {}", name); + return createVirtualClvmPool(name, host, path, type, details); + } } } @@ -1019,8 +1241,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { // to be always mounted, as long the primary storage isn't fully deleted. incStoragePoolRefCount(name); } - - if (sp.isActive() == 0) { + if (sp.isActive() == 0 && type != StoragePoolType.CLVM && type != StoragePoolType.CLVM_NG) { logger.debug("Attempting to activate pool " + name); sp.create(0); } @@ -1184,6 +1405,8 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { return (dataPool == null) ? createPhysicalDiskByLibVirt(name, pool, PhysicalDiskFormat.RAW, provisioningType, size) : createPhysicalDiskByQemuImg(name, pool, PhysicalDiskFormat.RAW, provisioningType, size, passphrase); + } else if (StoragePoolType.CLVM_NG.equals(poolType)) { + return createClvmNgDiskWithBacking(name, 0, size, null, pool); } else if (StoragePoolType.NetworkFilesystem.equals(poolType) || StoragePoolType.Filesystem.equals(poolType)) { switch (format) { case QCOW2: @@ -1301,9 +1524,71 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { return false; } } + + if (pool.getType() == StoragePoolType.CLVM_NG) { + ensureClvmNgBackingFileAccessible(name, pool); + } + return true; } + /** + * Checks if a CLVM_NG QCOW2 volume has a backing file (template) and ensures it's activated in shared mode. + * This is critical for multi-host deployments where VMs on different hosts need to access the same template. + * Called during VM deployment to ensure template backing files are accessible on the current host. + * + * @param volumeName The name of the volume (e.g., volume-uuid) + * @param pool The CLVM_NG storage pool + */ + private void ensureClvmNgBackingFileAccessible(String volumeName, KVMStoragePool pool) { + try { + String vgName = getVgName(pool.getLocalPath()); + String volumePath = "/dev/" + vgName + "/" + volumeName; + + logger.debug("Checking if CLVM_NG volume {} has a backing file that needs activation", volumePath); + + Script qemuImgInfo = new Script("qemu-img", Duration.millis(10000), logger); + qemuImgInfo.add("info"); + qemuImgInfo.add("--output=json"); + qemuImgInfo.add(volumePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = qemuImgInfo.execute(parser); + + if (result == null && parser.getLines() != null && !parser.getLines().isEmpty()) { + String jsonOutput = parser.getLines(); + + if (jsonOutput.contains("\"backing-filename\"")) { + int backingStart = jsonOutput.indexOf("\"backing-filename\""); + if (backingStart > 0) { + int valueStart = jsonOutput.indexOf(":", backingStart); + if (valueStart > 0) { + valueStart = jsonOutput.indexOf("\"", valueStart) + 1; + int valueEnd = jsonOutput.indexOf("\"", valueStart); + + if (valueEnd > valueStart) { + String backingFile = jsonOutput.substring(valueStart, valueEnd).trim(); + + if (!backingFile.isEmpty() && backingFile.startsWith("/dev/")) { + logger.info("Volume {} has backing file: {}. Ensuring backing file is in shared mode on this host.", + volumePath, backingFile); + ensureTemplateLvInSharedMode(backingFile, false); + } else { + logger.debug("Volume {} has backing file but not a block device path: {}", volumePath, backingFile); + } + } + } + } + } else { + logger.debug("Volume {} does not have a backing file (full clone)", volumePath); + } + } + } catch (Exception e) { + logger.warn("Failed to check/activate backing file for volume {}: {}. VM deployment may fail if template is not accessible.", + volumeName, e.getMessage()); + } + } + @Override public boolean disconnectPhysicalDisk(String uuid, KVMStoragePool pool) { // this is for managed storage that needs to cleanup disks after use @@ -1539,6 +1824,13 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { } else { try (KeyFile keyFile = new KeyFile(passphrase)){ String newUuid = name; + if (destPool.getType() == StoragePoolType.CLVM_NG && format == PhysicalDiskFormat.QCOW2) { + 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; + } List passphraseObjects = new ArrayList<>(); disk = destPool.createPhysicalDisk(newUuid, format, provisioningType, template.getVirtualSize(), passphrase); if (disk == null) { @@ -1552,14 +1844,6 @@ 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()) { @@ -1623,7 +1907,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { } private String getClvmBackingFile(KVMPhysicalDisk template, KVMStoragePool destPool) { - String templateLvName = "template-" + template.getName(); + String templateLvName = template.getName(); KVMPhysicalDisk templateOnPrimary = null; try { @@ -1665,30 +1949,16 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { 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 + boolean isActive = (lvAttr.indexOf('a') >= 0); + boolean isShared = (lvAttr.indexOf('s') >= 0); - if (activeChar != 'a' || sharedChar != 's') { - logger.info("Template LV {} is not in shared mode (attr: {}). Activating in shared mode.", + if (!isShared || !isActive) { + logger.info("Template LV {} is not in shared mode (attr: {}).", 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); - } + logger.info("Activating template LV {} in shared mode", templatePath); + LibvirtComputingResource.setClvmVolumeToSharedMode(templatePath); } else { - logger.debug("Template LV {} is already in shared mode", templatePath); + logger.debug("Template LV {} is already in shared mode (attr: {})", templatePath, lvAttr); } } } @@ -1937,23 +2207,75 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { disk.getFormat() : PhysicalDiskFormat.RAW; String sourcePath = disk.getPath(); - KVMPhysicalDisk newDisk; - logger.debug("copyPhysicalDisk: disk size:{}, virtualsize:{} format:{}", toHumanReadableSize(disk.getSize()), toHumanReadableSize(disk.getVirtualSize()), disk.getFormat()); - if (destPool.getType() != StoragePoolType.RBD) { - if (disk.getFormat() == PhysicalDiskFormat.TAR) { - newDisk = destPool.createPhysicalDisk(name, PhysicalDiskFormat.DIR, Storage.ProvisioningType.THIN, disk.getVirtualSize(), null); - } else { - newDisk = destPool.createPhysicalDisk(name, Storage.ProvisioningType.THIN, disk.getVirtualSize(), null); - } - } else { - newDisk = new KVMPhysicalDisk(destPool.getSourceDir() + "/" + name, name, destPool); - newDisk.setFormat(PhysicalDiskFormat.RAW); - newDisk.setSize(disk.getVirtualSize()); - newDisk.setVirtualSize(disk.getSize()); - } + boolean isSourceClvm = srcPool.getType() == StoragePoolType.CLVM || srcPool.getType() == StoragePoolType.CLVM_NG; + boolean isDestClvm = destPool.getType() == StoragePoolType.CLVM || destPool.getType() == StoragePoolType.CLVM_NG; - String destPath = newDisk.getPath(); - PhysicalDiskFormat destFormat = newDisk.getFormat(); + String clvmLockVolume = null; + boolean shouldDeactivateSource = false; + + try { + if (isSourceClvm) { + logger.info("Activating source CLVM volume {} in shared mode for copy", sourcePath); + LibvirtComputingResource.setClvmVolumeToSharedMode(sourcePath); + shouldDeactivateSource = !isDestClvm; + } + + KVMPhysicalDisk newDisk; + logger.debug("copyPhysicalDisk: disk size:{}, virtualsize:{} format:{}", toHumanReadableSize(disk.getSize()), toHumanReadableSize(disk.getVirtualSize()), disk.getFormat()); + if (destPool.getType() != StoragePoolType.RBD) { + if (disk.getFormat() == PhysicalDiskFormat.TAR) { + newDisk = destPool.createPhysicalDisk(name, PhysicalDiskFormat.DIR, Storage.ProvisioningType.THIN, disk.getVirtualSize(), null); + } else { + newDisk = destPool.createPhysicalDisk(name, Storage.ProvisioningType.THIN, disk.getVirtualSize(), null); + } + } else { + newDisk = new KVMPhysicalDisk(destPool.getSourceDir() + "/" + name, name, destPool); + newDisk.setFormat(PhysicalDiskFormat.RAW); + newDisk.setSize(disk.getVirtualSize()); + newDisk.setVirtualSize(disk.getSize()); + } + + String destPath = newDisk.getPath(); + PhysicalDiskFormat destFormat = newDisk.getFormat(); + + if (isDestClvm) { + logger.info("Activating destination CLVM volume {} in shared mode for copy", destPath); + LibvirtComputingResource.setClvmVolumeToSharedMode(destPath); + clvmLockVolume = destPath; + } + + boolean formatConversion = sourceFormat != destFormat; + if (formatConversion) { + logger.info("Format conversion required: {} -> {}", sourceFormat, destFormat); + } + + return performCopy(disk, name, destPool, timeout, srcPool, sourceFormat, + sourcePath, newDisk, destPath, destFormat, formatConversion); + + } finally { + if (isSourceClvm && shouldDeactivateSource) { + try { + logger.info("Deactivating source CLVM volume {} after copy", sourcePath); + LibvirtComputingResource.deactivateClvmVolume(sourcePath); + } catch (Exception e) { + logger.warn("Failed to deactivate source CLVM volume {}: {}", sourcePath, e.getMessage()); + } + } + if (isDestClvm && clvmLockVolume != null) { + try { + logger.info("Claiming exclusive lock on destination CLVM volume {} after copy", clvmLockVolume); + LibvirtComputingResource.activateClvmVolumeExclusive(clvmLockVolume); + } catch (Exception e) { + logger.warn("Failed to claim exclusive lock on destination CLVM volume {}: {}", clvmLockVolume, e.getMessage()); + } + } + } + } + + private KVMPhysicalDisk performCopy(KVMPhysicalDisk disk, String name, KVMStoragePool destPool, int timeout, + KVMStoragePool srcPool, PhysicalDiskFormat sourceFormat, String sourcePath, + KVMPhysicalDisk newDisk, String destPath, PhysicalDiskFormat destFormat, + boolean formatConversion) { QemuImg qemu; @@ -2183,33 +2505,49 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { /** * 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 + * @return PE size in bytes, defaults to 4MiB if it cannot be determined */ private long getVgPhysicalExtentSize(String vgName) { + final long DEFAULT_PE_SIZE = 4 * 1024 * 1024L; 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("--units", "b"); + vgDisplay.add("-C"); 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); + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = vgDisplay.execute(parser); + + if (result != null) { + logger.warn("{}: {}", warningMessage, result); + return DEFAULT_PE_SIZE; } + + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + logger.warn("{}: empty output", warningMessage); + return DEFAULT_PE_SIZE; + } + + output = output.trim(); + if (output.endsWith("B")) { + output = output.substring(0, output.length() - 1).trim(); + } + + long peSize = Long.parseLong(output); + logger.debug("Physical Extent size for VG {} is {} bytes", vgName, peSize); + return peSize; + + } catch (NumberFormatException e) { + logger.warn("{}: failed to parse PE size", warningMessage, e); } catch (Exception e) { - logger.warn(warningMessage, e.getMessage()); + 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; } @@ -2233,16 +2571,46 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { return finalSize; } + private long getQcow2VirtualSize(String imagePath) { + Script qemuImg = new Script("qemu-img", 300000, logger); + qemuImg.add("info"); + qemuImg.add("--output=json"); + qemuImg.add(imagePath); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = qemuImg.execute(parser); + + if (result != null) { + throw new CloudRuntimeException("Failed to get QCOW2 virtual size for " + imagePath + ": " + result); + } + + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + throw new CloudRuntimeException("qemu-img info returned empty output for " + imagePath); + } + + JsonObject info = JsonParser.parseString(output).getAsJsonObject(); + return info.get("virtual-size").getAsLong(); + } + - /** - * 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(); + + OutputInterpreter.AllLinesParser parser = new OutputInterpreter.AllLinesParser(); + String result = qemuImg.execute(parser); + + if (result != null) { + throw new CloudRuntimeException("Failed to get QCOW2 physical size for " + imagePath + ": " + result); + } + + String output = parser.getLines(); + if (output == null || output.trim().isEmpty()) { + throw new CloudRuntimeException("qemu-img info returned empty output for " + imagePath); + } JsonObject info = JsonParser.parseString(output).getAsJsonObject(); return info.get("actual-size").getAsLong(); @@ -2313,8 +2681,10 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { logger.info("Creating new template LV {} in VG {} for template {}", lvName, vgName, templateUuid); - long physicalSize = getQcow2PhysicalSize(templatePath); - long lvSize = calculateClvmNgLvSize(physicalSize, vgName); + // TODO: need to verify if virtual / physical size is to be used! + //long physicalSize = getQcow2PhysicalSize(templatePath); + long virualSize = getQcow2VirtualSize(templatePath); + long lvSize = calculateClvmNgLvSize(virualSize, vgName); Script lvcreate = new Script("lvcreate", Duration.millis(timeout), logger); lvcreate.add("-n", lvName); @@ -2339,7 +2709,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { } logger.info("Created template LV {} with size {} bytes (physical: {}, overhead: {})", - lvName, lvSize, physicalSize, lvSize - physicalSize); + lvName, lvSize, virualSize, lvSize - virualSize); try { ensureTemplateLvInSharedMode(lvPath, true); @@ -2351,7 +2721,7 @@ public class LibvirtStorageAdaptor implements StorageAdaptor { KVMPhysicalDisk templateDisk = new KVMPhysicalDisk(lvPath, lvName, pool); templateDisk.setFormat(PhysicalDiskFormat.QCOW2); - templateDisk.setVirtualSize(physicalSize); + templateDisk.setVirtualSize(virualSize); templateDisk.setSize(lvSize); } diff --git a/scripts/storage/qcow2/managesnapshot.sh b/scripts/storage/qcow2/managesnapshot.sh index 697ee1d4222..a575fdd4ca2 100755 --- a/scripts/storage/qcow2/managesnapshot.sh +++ b/scripts/storage/qcow2/managesnapshot.sh @@ -65,6 +65,20 @@ get_lv() { lvm lvs --noheadings --unbuffered --separator=/ "${1}" | cut -d '/' -f 1 | tr -d ' ' } +# Check if a block device contains QCOW2 data (CLVM_NG) +is_qcow2_on_block_device() { + local disk=$1 + + # Must be a block device + if [ ! -b "${disk}" ]; then + return 1 + fi + + # Check if it contains QCOW2 data using qemu-img info + ${qemu_img} info "${disk}" 2>/dev/null | grep -q "file format: qcow2" + return $? +} + create_snapshot() { local disk=$1 local snapshotname="$2" @@ -73,7 +87,6 @@ create_snapshot() { islv_ret=$? if [ ${dmsnapshot} = "yes" ] && [ "$islv_ret" == "1" ]; then - # Modern LVM snapshot approach local lv=`get_lv ${disk}` local vg=`get_vg ${disk}` local lv_bytes=`blockdev --getsize64 ${disk}` @@ -133,11 +146,18 @@ destroy_snapshot() { local disk=$1 local snapshotname="$2" local failed=0 + + # If the disk path does not exist any more, assume volume was deleted and + # the snapshot is already gone — return success to let caller proceed. + if [ ! -e "${disk}" ]; then + printf "Disk %s does not exist; assuming volume removed and snapshot %s is deleted\n" "${disk}" "${snapshotname}" >&2 + return 0 + fi + is_lv ${disk} islv_ret=$? if [ "$islv_ret" == "1" ]; then - # Modern LVM snapshot deletion local lv=`get_lv ${disk}` local vg=`get_vg ${disk}` @@ -147,7 +167,6 @@ destroy_snapshot() { return 0 fi - # Remove the snapshot using native LVM command lvm lvremove -f "${vg}/${snapshotname}" >&2 if [ $? -gt 0 ]; then printf "***Failed to remove LVM snapshot ${vg}/${snapshotname}\n" >&2 @@ -176,7 +195,6 @@ rollback_snapshot() { islv_ret=$? if [ ${dmrollback} = "yes" ] && [ "$islv_ret" == "1" ]; then - # Modern LVM snapshot merge (rollback) local lv=`get_lv ${disk}` local vg=`get_vg ${disk}` @@ -229,9 +247,11 @@ backup_snapshot() { is_lv ${disk} islv_ret=$? + # Both CLVM and CLVM_NG use LVM snapshot backup, but with different formats if [ ${dmsnapshot} = "yes" ] && [ "$islv_ret" == "1" ] ; then local vg=`get_vg ${disk}` local scriptdir=`dirname ${0}` + local input_format="raw" # Check if snapshot exists using native LVM command if ! lvm lvs "${vg}/${snapshotname}" > /dev/null 2>&1; then @@ -239,13 +259,19 @@ backup_snapshot() { return 1 fi - # Use native LVM path for backup - qemuimg_ret=$($qemu_img convert $forceShareFlag -f raw -O qcow2 "/dev/${vg}/${snapshotname}" "${destPath}/${destName}" 2>&1) + # Detect if this is CLVM_NG (QCOW2 on block device) + if is_qcow2_on_block_device "${disk}"; then + input_format="qcow2" + printf "Detected CLVM_NG volume, using qcow2 format for backup\n" >&2 + fi + + # Use native LVM path for backup with appropriate format + qemuimg_ret=$($qemu_img convert $forceShareFlag -f ${input_format} -O qcow2 "/dev/${vg}/${snapshotname}" "${destPath}/${destName}" 2>&1) ret_code=$? if [ $ret_code -gt 0 ] && ([[ $qemuimg_ret == *"invalid option"*"'U'"* ]] || [[ $qemuimg_ret == *"unrecognized option"*"'-U'"* ]]) then forceShareFlag="" - $qemu_img convert $forceShareFlag -f raw -O qcow2 "/dev/${vg}/${snapshotname}" "${destPath}/${destName}" + $qemu_img convert $forceShareFlag -f ${input_format} -O qcow2 "/dev/${vg}/${snapshotname}" "${destPath}/${destName}" ret_code=$? fi if [ $ret_code -gt 0 ] @@ -308,8 +334,15 @@ revert_snapshot() { local snapshotPath=$1 local destPath=$2 local output_format="qcow2" + + # Check if destination is a block device if [ -b "$destPath" ]; then - output_format="raw" + if is_qcow2_on_block_device "${destPath}"; then + output_format="qcow2" + printf "Detected CLVM_NG volume %s, preserving QCOW2 format for revert\n" "${destPath}" >&2 + else + output_format="raw" + fi fi ${qemu_img} convert -f qcow2 -O ${output_format} "$snapshotPath" "$destPath" || \ diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java index 1f453a86294..92b3659eb81 100644 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@ -1052,6 +1052,7 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C params.put("managed", cmd.isManaged()); params.put("capacityBytes", cmd.getCapacityBytes()); params.put("capacityIops", cmd.getCapacityIops()); + params.put("scheme", uriParams.get("scheme")); if (MapUtils.isNotEmpty(uriParams)) { params.putAll(uriParams); } @@ -1137,6 +1138,10 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C if (isHostOrPathBlank) { throw new InvalidParameterValueException("host or path is null, should be gluster://hostname/volume"); } + } else if (scheme.equalsIgnoreCase("clvm") || scheme.equalsIgnoreCase("clvm_ng")) { + if (storagePath == null) { + throw new InvalidParameterValueException("path is null, should be " + scheme.toLowerCase() + "://localhost/volume-group-name"); + } } String hostPath = null; diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java index 8109442acab..2ebe2b9522a 100755 --- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java @@ -1667,7 +1667,7 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement if (backupSnapToSecondary) { if (!isKvmAndFileBasedStorage) { backupSnapshotToSecondary(payload.getAsyncBackup(), snapshotStrategy, snapshotOnPrimary, payload.getZoneIds(), payload.getStoragePoolIds()); - if (storagePool.getPoolType() == StoragePoolType.CLVM) { + if (storagePool.getPoolType() == StoragePoolType.CLVM || storagePool.getPoolType() == StoragePoolType.CLVM_NG) { _snapshotStoreDao.removeBySnapshotStore(snapshotId, snapshotOnPrimary.getDataStore().getId(), snapshotOnPrimary.getDataStore().getRole()); } } else { diff --git a/ui/src/views/infra/AddPrimaryStorage.vue b/ui/src/views/infra/AddPrimaryStorage.vue index 4d7d2f64663..48dd363e0df 100644 --- a/ui/src/views/infra/AddPrimaryStorage.vue +++ b/ui/src/views/infra/AddPrimaryStorage.vue @@ -863,7 +863,7 @@ export default { 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 + 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) diff --git a/utils/src/main/java/com/cloud/utils/UriUtils.java b/utils/src/main/java/com/cloud/utils/UriUtils.java index eba6f88201b..2e8d6a60284 100644 --- a/utils/src/main/java/com/cloud/utils/UriUtils.java +++ b/utils/src/main/java/com/cloud/utils/UriUtils.java @@ -638,6 +638,9 @@ public class UriUtils { if (url.startsWith("rbd://")) { return getRbdUrlInfo(url); } + if (url.startsWith("clvm://") || url.startsWith("clvm_ng://")) { + return getClvmUrlInfo(url); + } URI uri = new URI(UriUtils.encodeURIComponent(url)); return new UriInfo(uri.getScheme(), uri.getHost(), uri.getPath(), uri.getUserInfo(), uri.getPort()); } catch (URISyntaxException e) { @@ -675,6 +678,36 @@ public class UriUtils { } } + private static UriInfo getClvmUrlInfo(String url) { + if (url == null || (!url.toLowerCase().startsWith("clvm://") && !url.toLowerCase().startsWith("clvm_ng://"))) { + throw new CloudRuntimeException("CLVM URL must start with \"clvm://\" or \"clvm_ng://\""); + } + + String scheme; + String remainder; + if (url.toLowerCase().startsWith("clvm_ng://")) { + scheme = "clvm_ng"; + remainder = url.substring(10); + } else { + scheme = "clvm"; + remainder = url.substring(7); + } + + int firstSlash = remainder.indexOf('/'); + String host = (firstSlash == -1) ? remainder : remainder.substring(0, firstSlash); + String path = (firstSlash == -1) ? "/" : remainder.substring(firstSlash); + + if (host.isEmpty()) { + host = "localhost"; + } + + while (path.startsWith("//")) { + path = path.substring(1); + } + + return new UriInfo(scheme, host, path, null, -1); + } + public static boolean isUrlForCompressedFile(String url) { return UriUtils.COMPRESSION_FORMATS.stream().anyMatch(f -> url.toLowerCase().endsWith(f)); }