CLVM enhancements and fixes

This commit is contained in:
Pearl Dsilva 2026-02-09 14:14:48 -05:00
parent d3e1976912
commit 9e03f4bb48
3 changed files with 269 additions and 16 deletions

View File

@ -1115,7 +1115,14 @@ public class KVMStorageProcessor implements StorageProcessor {
}
} else {
final Script command = new Script(_manageSnapshotPath, cmd.getWaitInMillSeconds(), logger);
command.add("-b", isCreatedFromVmSnapshot ? snapshotDisk.getPath() : snapshot.getPath());
String backupPath;
if (primaryPool.getType() == StoragePoolType.CLVM) {
backupPath = snapshotDisk.getPath();
logger.debug("Using snapshotDisk path for CLVM backup: " + backupPath);
} else {
backupPath = isCreatedFromVmSnapshot ? snapshotDisk.getPath() : snapshot.getPath();
}
command.add("-b", backupPath);
command.add(NAME_OPTION, snapshotName);
command.add("-p", snapshotDestPath);
@ -1172,7 +1179,11 @@ public class KVMStorageProcessor implements StorageProcessor {
if ((backupSnapshotAfterTakingSnapshot == null || BooleanUtils.toBoolean(backupSnapshotAfterTakingSnapshot)) && deleteSnapshotOnPrimary) {
try {
Files.deleteIfExists(Paths.get(snapshotPath));
if (primaryPool.getType() == StoragePoolType.CLVM) {
deleteClvmSnapshot(snapshotPath);
} else {
Files.deleteIfExists(Paths.get(snapshotPath));
}
} catch (IOException ex) {
logger.error("Failed to delete snapshot [{}] on primary storage [{}].", snapshot.getId(), snapshot.getName(), ex);
}
@ -1181,6 +1192,81 @@ public class KVMStorageProcessor implements StorageProcessor {
}
}
/**
* Delete a CLVM snapshot using lvremove command.
* For CLVM, the snapshot path stored in DB is: /dev/vgname/volumeuuid/snapshotuuid
* However, managesnapshot.sh creates the actual snapshot using MD5 hash of the snapshot UUID.
* The actual device is at: /dev/mapper/vgname-MD5(snapshotuuid)
* We need to compute the MD5 hash and remove both the snapshot LV and its COW volume.
*/
private void deleteClvmSnapshot(String snapshotPath) {
try {
// Parse the snapshot path: /dev/acsvg/volume-uuid/snapshot-uuid
// Extract VG name and snapshot UUID
String[] pathParts = snapshotPath.split("/");
if (pathParts.length < 5) {
logger.warn("Invalid CLVM snapshot path format: " + snapshotPath + ", skipping deletion");
return;
}
String vgName = pathParts[2];
String snapshotUuid = pathParts[4];
// Compute MD5 hash of snapshot UUID (same as managesnapshot.sh does)
String md5Hash = computeMd5Hash(snapshotUuid);
logger.debug("Deleting CLVM snapshot for UUID: " + snapshotUuid + " (MD5: " + md5Hash + ")");
// Remove the snapshot device mapper entry
// The snapshot device is at: /dev/mapper/vgname-md5hash
String vgNameEscaped = vgName.replace("-", "--");
String snapshotDevice = vgNameEscaped + "-" + md5Hash;
Script dmRemoveCmd = new Script("/usr/sbin/dmsetup", 30000, logger);
dmRemoveCmd.add("remove");
dmRemoveCmd.add(snapshotDevice);
String dmResult = dmRemoveCmd.execute();
if (dmResult != null) {
logger.debug("dmsetup remove returned: {} (may already be removed)", dmResult);
}
// Remove the COW (copy-on-write) volume: /dev/vgname/md5hash-cow
String cowLvPath = "/dev/" + vgName + "/" + md5Hash + "-cow";
Script removeCowCmd = new Script("/usr/sbin/lvremove", 30000, logger);
removeCowCmd.add("-f");
removeCowCmd.add(cowLvPath);
String cowResult = removeCowCmd.execute();
if (cowResult != null) {
logger.warn("Failed to remove CLVM COW volume {} : {}",cowLvPath, cowResult);
} else {
logger.debug("Successfully deleted CLVM snapshot COW volume: {}", cowLvPath);
}
} catch (Exception ex) {
logger.error("Exception while deleting CLVM snapshot {}", snapshotPath, ex);
}
}
/**
* Compute MD5 hash of a string, matching what managesnapshot.sh does:
* echo "${snapshot}" | md5sum -t | awk '{ print $1 }'
*/
private String computeMd5Hash(String input) {
try {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
byte[] array = md.digest((input + "\n").getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : array) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
logger.error("Failed to compute MD5 hash for: {}", input, e);
return input;
}
}
protected synchronized void attachOrDetachISO(final Connect conn, final String vmName, String isoPath, final boolean isAttach, Map<String, String> params, DataStoreTO store) throws
LibvirtException, InternalErrorException {
DiskDef iso = new DiskDef();
@ -1842,8 +1928,14 @@ public class KVMStorageProcessor implements StorageProcessor {
}
}
if (DomainInfo.DomainState.VIR_DOMAIN_RUNNING.equals(state) && volume.requiresEncryption()) {
throw new CloudRuntimeException("VM is running, encrypted volume snapshots aren't supported");
if (DomainInfo.DomainState.VIR_DOMAIN_RUNNING.equals(state)) {
if (volume.requiresEncryption()) {
throw new CloudRuntimeException("VM is running, encrypted volume snapshots aren't supported");
}
if (StoragePoolType.CLVM.name().equals(primaryStore.getType())) {
throw new CloudRuntimeException("VM is running, live snapshots aren't supported with CLVM primary storage");
}
}
KVMStoragePool primaryPool = storagePoolMgr.getStoragePool(primaryStore.getPoolType(), primaryStore.getUuid());

View File

@ -34,6 +34,7 @@ import java.util.stream.Collectors;
import com.cloud.agent.properties.AgentProperties;
import com.cloud.agent.properties.AgentPropertiesFileHandler;
import com.cloud.utils.script.OutputInterpreter;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.utils.cryptsetup.KeyFile;
import org.apache.cloudstack.utils.qemu.QemuImageOptions;
@ -254,9 +255,12 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
try {
vol = pool.storageVolLookupByName(volName);
logger.debug("Found volume " + volName + " in storage pool " + pool.getName() + " after refreshing the pool");
if (vol != null) {
logger.debug("Found volume " + volName + " in storage pool " + pool.getName() + " after refreshing the pool");
}
} catch (LibvirtException e) {
throw new CloudRuntimeException("Could not find volume " + volName + ": " + e.getMessage());
logger.debug("Volume " + volName + " still not found after pool refresh: " + e.getMessage());
return null;
}
}
@ -663,6 +667,17 @@ 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
if (vol == null) {
logger.debug("Volume " + volumeUuid + " not found in libvirt, will check for CLVM fallback");
if (pool.getType() == StoragePoolType.CLVM) {
return getPhysicalDisk(volumeUuid, pool, libvirtPool);
}
throw new CloudRuntimeException("Volume " + volumeUuid + " not found in libvirt pool");
}
KVMPhysicalDisk disk;
LibvirtStorageVolumeDef voldef = getStorageVolumeDef(libvirtPool.getPool().getConnect(), vol);
disk = new KVMPhysicalDisk(vol.getPath(), vol.getName(), pool);
@ -693,11 +708,153 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
}
return disk;
} catch (LibvirtException e) {
logger.debug("Failed to get physical disk:", 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) {
return getPhysicalDisk(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);
try {
logger.debug("Refreshing libvirt storage pool: {}", pool.getUuid());
libvirtPool.getPool().refresh(0);
StorageVol vol = getVolume(libvirtPool.getPool(), volumeUuid);
if (vol != null) {
logger.info("Volume found after pool refresh: {}", volumeUuid);
KVMPhysicalDisk disk;
LibvirtStorageVolumeDef voldef = getStorageVolumeDef(libvirtPool.getPool().getConnect(), vol);
disk = new KVMPhysicalDisk(vol.getPath(), vol.getName(), pool);
disk.setSize(vol.getInfo().allocation);
disk.setVirtualSize(vol.getInfo().capacity);
disk.setFormat(voldef.getFormat() == LibvirtStorageVolumeDef.VolumeFormat.QCOW2 ?
PhysicalDiskFormat.QCOW2 : PhysicalDiskFormat.RAW);
return disk;
}
} catch (LibvirtException refreshEx) {
logger.debug("Pool refresh failed or volume still not found: {}", refreshEx.getMessage());
}
// Still not found after refresh, try direct block device access
return getPhysicalDiskViaDirectBlockDevice(volumeUuid, pool);
}
/**
* For CLVM volumes that exist in LVM but are not visible to libvirt,
* access them directly via block device path.
*/
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 = sourceDir;
if (vgName.startsWith("/")) {
String[] parts = vgName.split("/");
List<String> tokens = Arrays.stream(parts)
.filter(s -> !s.isEmpty()).collect(Collectors.toList());
vgName = tokens.size() > 1 ? tokens.get(1)
: tokens.size() == 1 ? tokens.get(0)
: "";
}
logger.debug("Using VG name: {} (from sourceDir: {}) ", vgName, sourceDir);
// Check if the LV exists in LVM using lvs command
logger.debug("Checking if volume {} exsits 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));
}
logger.info("Volume {} exists in LVM but not visible to libvirt, accessing directly", volumeUuid);
// 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);
disk.setFormat(PhysicalDiskFormat.RAW);
disk.setSize(size);
disk.setVirtualSize(size);
logger.info("Successfully accessed CLVM volume via direct block device: {} " +
"with size: {} bytes",lvPath, size);
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()));
}
}
/**
* adjust refcount
*/
@ -1227,7 +1384,11 @@ public class LibvirtStorageAdaptor implements StorageAdaptor {
LibvirtStoragePool libvirtPool = (LibvirtStoragePool)pool;
try {
StorageVol vol = getVolume(libvirtPool.getPool(), uuid);
logger.debug("Instructing libvirt to remove volume " + uuid + " from pool " + pool.getUuid());
if (vol == null) {
logger.warn("Volume %s not found in libvirt pool %s, it may have been already deleted", uuid, pool.getUuid());
return true;
}
logger.debug("Instructing libvirt to remove volume %s from pool %s", uuid, pool.getUuid());
if(Storage.ImageFormat.DIR.equals(format)){
deleteDirVol(libvirtPool, vol);
} else {

View File

@ -212,12 +212,12 @@ backup_snapshot() {
return 1
fi
qemuimg_ret=$($qemu_img $forceShareFlag -f raw -O qcow2 "/dev/mapper/${vg_dm}-${snapshotname}" "${destPath}/${destName}")
qemuimg_ret=$($qemu_img convert $forceShareFlag -f raw -O qcow2 "/dev/mapper/${vg_dm}-${snapshotname}" "${destPath}/${destName}" 2>&1)
ret_code=$?
if [ $ret_code -gt 0 ] && [[ $qemuimg_ret == *"snapshot: invalid option -- 'U'"* ]]
if [ $ret_code -gt 0 ] && ([[ $qemuimg_ret == *"invalid option"*"'U'"* ]] || [[ $qemuimg_ret == *"unrecognized option"*"'-U'"* ]])
then
forceShareFlag=""
$qemu_img $forceShareFlag -f raw -O qcow2 "/dev/mapper/${vg_dm}-${snapshotname}" "${destPath}/${destName}"
$qemu_img convert $forceShareFlag -f raw -O qcow2 "/dev/mapper/${vg_dm}-${snapshotname}" "${destPath}/${destName}"
ret_code=$?
fi
if [ $ret_code -gt 0 ]
@ -240,9 +240,9 @@ backup_snapshot() {
# Backup VM snapshot
qemuimg_ret=$($qemu_img snapshot $forceShareFlag -l $disk 2>&1)
ret_code=$?
if [ $ret_code -gt 0 ] && [[ $qemuimg_ret == *"snapshot: invalid option -- 'U'"* ]]; then
if [ $ret_code -gt 0 ] && ([[ $qemuimg_ret == *"invalid option"*"'U'"* ]] || [[ $qemuimg_ret == *"unrecognized option"*"'-U'"* ]]); then
forceShareFlag=""
qemuimg_ret=$($qemu_img snapshot $forceShareFlag -l $disk)
qemuimg_ret=$($qemu_img snapshot $forceShareFlag -l $disk 2>&1)
ret_code=$?
fi
@ -251,11 +251,11 @@ backup_snapshot() {
return 1
fi
qemuimg_ret=$($qemu_img convert $forceShareFlag -f qcow2 -O qcow2 -l snapshot.name=$snapshotname $disk $destPath/$destName 2>&1 > /dev/null)
qemuimg_ret=$($qemu_img convert $forceShareFlag -f qcow2 -O qcow2 -l snapshot.name=$snapshotname $disk $destPath/$destName 2>&1)
ret_code=$?
if [ $ret_code -gt 0 ] && [[ $qemuimg_ret == *"convert: invalid option -- 'U'"* ]]; then
if [ $ret_code -gt 0 ] && ([[ $qemuimg_ret == *"invalid option"*"'U'"* ]] || [[ $qemuimg_ret == *"unrecognized option"*"'-U'"* ]]); then
forceShareFlag=""
qemuimg_ret=$($qemu_img convert $forceShareFlag -f qcow2 -O qcow2 -l snapshot.name=$snapshotname $disk $destPath/$destName 2>&1 > /dev/null)
qemuimg_ret=$($qemu_img convert $forceShareFlag -f qcow2 -O qcow2 -l snapshot.name=$snapshotname $disk $destPath/$destName 2>&1)
ret_code=$?
fi