Add support for clvm_ng - which allows qcow2 on block storage , linked clones, etc

This commit is contained in:
Pearl Dsilva 2026-03-13 17:12:15 -04:00
parent 81bb667267
commit cc924c5b3a
9 changed files with 754 additions and 285 deletions

View File

@ -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.

View File

@ -113,7 +113,7 @@ public final class LibvirtResizeVolumeCommandWrapper extends CommandWrapper<Resi
logger.debug("Resizing volume: " + path + ", from: " + toHumanReadableSize(currentSize) + ", to: " + toHumanReadableSize(newSize) + ", type: " + type + ", name: " + vmInstanceName + ", shrinkOk: " + shrinkOk);
/* libvirt doesn't support resizing (C)LVM devices, and corrupts QCOW2 in some scenarios, so we have to do these via qemu-img */
if (pool.getType() != StoragePoolType.CLVM && pool.getType() != StoragePoolType.CLVM_NG
if (pool.getType() != StoragePoolType.CLVM && pool.getType() != StoragePoolType.CLVM_NG
&& pool.getType() != StoragePoolType.Linstor && pool.getType() != StoragePoolType.PowerFlex
&& vol.getFormat() != PhysicalDiskFormat.QCOW2) {
logger.debug("Volume " + path + " can be resized by libvirt. Asking libvirt to resize the volume.");

View File

@ -1129,9 +1129,9 @@ public class KVMStorageProcessor implements StorageProcessor {
} else {
final Script command = new Script(_manageSnapshotPath, cmd.getWaitInMillSeconds(), logger);
String backupPath;
if (primaryPool.getType() == StoragePoolType.CLVM) {
if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) {
backupPath = snapshotDisk.getPath();
logger.debug("Using snapshotDisk path for CLVM backup: " + backupPath);
logger.debug("Using snapshotDisk path for CLVM/CLVM_NG backup: " + backupPath);
} else {
backupPath = isCreatedFromVmSnapshot ? snapshotDisk.getPath() : snapshot.getPath();
}
@ -1181,71 +1181,85 @@ public class KVMStorageProcessor implements StorageProcessor {
}
/**
* Delete a CLVM snapshot using comprehensive cleanup.
* For CLVM, the snapshot path stored in DB is: /dev/vgname/volumeuuid/snapshotuuid
* the actual snapshot LV created by CLVM is: /dev/vgname/md5(snapshotuuid)
* Parse CLVM/CLVM_NG snapshot path and compute MD5 hash.
* Snapshot path format: /dev/vgname/volumeuuid/snapshotuuid
*
* @param snapshotPath The snapshot path from database
* @param poolType Storage pool type (for logging)
* @return Array of [vgName, volumeUuid, snapshotUuid, md5Hash] or null if invalid
*/
private String[] parseClvmSnapshotPath(String snapshotPath, StoragePoolType poolType) {
String[] pathParts = snapshotPath.split("/");
if (pathParts.length < 5) {
logger.warn("Invalid {} snapshot path format: {}, expected format: /dev/vgname/volume-uuid/snapshot-uuid",
poolType, snapshotPath);
return null;
}
String vgName = pathParts[2];
String volumeUuid = pathParts[3];
String snapshotUuid = pathParts[4];
logger.info("Parsed {} snapshot path - VG: {}, Volume: {}, Snapshot: {}",
poolType, vgName, volumeUuid, snapshotUuid);
String md5Hash = computeMd5Hash(snapshotUuid);
logger.debug("Computed MD5 hash for snapshot UUID {}: {}", snapshotUuid, md5Hash);
return new String[]{vgName, volumeUuid, snapshotUuid, md5Hash};
}
/**
* Delete a CLVM or CLVM_NG snapshot using managesnapshot.sh script.
* For both CLVM and CLVM_NG, the snapshot path stored in DB is: /dev/vgname/volumeuuid/snapshotuuid
* The script handles MD5 transformation and pool-specific deletion commands internally:
* - CLVM: Uses lvremove to delete LVM snapshot
* - CLVM_NG: Uses qemu-img snapshot -d to delete QCOW2 internal snapshot
* This approach is consistent with snapshot creation and backup which also use the script.
*
* @param snapshotPath The snapshot path from database
* @param poolType Storage pool type (CLVM or CLVM_NG)
* @param checkExistence If true, checks if snapshot exists before cleanup (for explicit deletion)
* If false, always performs cleanup (for post-backup cleanup)
* @return true if cleanup was performed, false if snapshot didn't exist (when checkExistence=true)
*/
private boolean deleteClvmSnapshot(String snapshotPath, boolean checkExistence) {
logger.info("Starting CLVM snapshot deletion for path: {}, checkExistence: {}", snapshotPath, checkExistence);
private boolean deleteClvmSnapshot(String snapshotPath, StoragePoolType poolType, boolean checkExistence) {
logger.info("Starting {} snapshot deletion for path: {}, checkExistence: {}", poolType, snapshotPath, checkExistence);
try {
// Parse the snapshot path: /dev/acsvg/volume-uuid/snapshot-uuid
String[] pathParts = snapshotPath.split("/");
if (pathParts.length < 5) {
logger.warn("Invalid CLVM snapshot path format: {}, expected format: /dev/vgname/volume-uuid/snapshot-uuid", snapshotPath);
String[] parsed = parseClvmSnapshotPath(snapshotPath, poolType);
if (parsed == null) {
return false;
}
String vgName = pathParts[2];
String volumeUuid = pathParts[3];
String snapshotUuid = pathParts[4];
String vgName = parsed[0];
String volumeUuid = parsed[1];
String snapshotUuid = parsed[2];
String volumePath = "/dev/" + vgName + "/" + volumeUuid;
logger.info("Parsed snapshot path - VG: {}, Volume: {}, Snapshot: {}", vgName, volumeUuid, snapshotUuid);
// Use managesnapshot.sh script for deletion (consistent with create/backup)
// Script handles MD5 transformation and pool-specific commands internally
Script deleteCommand = new Script(_manageSnapshotPath, 10000, logger);
deleteCommand.add("-d", volumePath);
deleteCommand.add("-n", snapshotUuid);
// Compute MD5 hash of snapshot UUID (same as managesnapshot.sh does)
String md5Hash = computeMd5Hash(snapshotUuid);
logger.debug("Computed MD5 hash for snapshot UUID {}: {}", snapshotUuid, md5Hash);
String snapshotLvPath = vgName + "/" + md5Hash;
String actualSnapshotPath = "/dev/" + snapshotLvPath;
logger.info("Executing: managesnapshot.sh -d {} -n {}", volumePath, snapshotUuid);
String result = deleteCommand.execute();
// Check if snapshot exists (if requested)
if (checkExistence) {
Script checkSnapshot = new Script("/usr/sbin/lvs", 5000, logger);
checkSnapshot.add("--noheadings");
checkSnapshot.add(snapshotLvPath);
String checkResult = checkSnapshot.execute();
if (checkResult != null) {
// Snapshot doesn't exist - was already cleaned up
logger.info("CLVM snapshot {} was already deleted, no cleanup needed", md5Hash);
return false;
}
logger.info("CLVM snapshot still exists for {}, performing cleanup", md5Hash);
}
// Use native LVM command to remove snapshot (handles all cleanup automatically)
Script removeSnapshot = new Script("lvremove", 10000, logger);
removeSnapshot.add("-f");
removeSnapshot.add(snapshotLvPath);
logger.info("Executing: lvremove -f {}", snapshotLvPath);
String removeResult = removeSnapshot.execute();
if (removeResult == null) {
logger.info("Successfully deleted CLVM snapshot: {} (actual path: {})", snapshotPath, actualSnapshotPath);
if (result == null) {
logger.info("Successfully deleted {} snapshot: {}", poolType, snapshotPath);
return true;
} else {
logger.warn("Failed to delete CLVM snapshot {}: {}", snapshotPath, removeResult);
if (checkExistence && result.contains("does not exist")) {
logger.info("{} snapshot {} already deleted, no cleanup needed", poolType, snapshotPath);
return true;
}
logger.warn("Failed to delete {} snapshot {}: {}", poolType, snapshotPath, result);
return false;
}
} catch (Exception ex) {
logger.error("Exception while deleting CLVM snapshot {}", snapshotPath, ex);
logger.error("Exception while deleting {} snapshot {}", poolType, snapshotPath, ex);
return false;
}
}
@ -1262,10 +1276,11 @@ public class KVMStorageProcessor implements StorageProcessor {
if ((backupSnapshotAfterTakingSnapshot == null || BooleanUtils.toBoolean(backupSnapshotAfterTakingSnapshot)) && deleteSnapshotOnPrimary) {
try {
if (primaryPool.getType() == StoragePoolType.CLVM) {
boolean cleanedUp = deleteClvmSnapshot(snapshotPath, false);
if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) {
// Both CLVM and CLVM_NG use the same deletion method via managesnapshot.sh script
boolean cleanedUp = deleteClvmSnapshot(snapshotPath, primaryPool.getType(), false);
if (!cleanedUp) {
logger.info("No need to delete CLVM snapshot on primary as it doesn't exist: {}", snapshotPath);
logger.info("No need to delete {} snapshot on primary as it doesn't exist: {}", primaryPool.getType(), snapshotPath);
}
} else {
Files.deleteIfExists(Paths.get(snapshotPath));
@ -1637,6 +1652,10 @@ public class KVMStorageProcessor implements StorageProcessor {
if (attachingDisk.getFormat() == PhysicalDiskFormat.QCOW2) {
diskdef.setDiskFormatType(DiskDef.DiskFmtType.QCOW2);
}
} else if (attachingPool.getType() == StoragePoolType.CLVM_NG) {
// CLVM_NG uses QCOW2 format on block devices
diskdef.defBlockBasedDisk(attachingDisk.getPath(), devId, busT);
diskdef.setDiskFormatType(DiskDef.DiskFmtType.QCOW2);
} else if (attachingDisk.getFormat() == PhysicalDiskFormat.QCOW2) {
diskdef.defFileBasedDisk(attachingDisk.getPath(), devId, busT, DiskDef.DiskFmtType.QCOW2);
} else if (attachingDisk.getFormat() == PhysicalDiskFormat.RAW) {
@ -1986,7 +2005,7 @@ public class KVMStorageProcessor implements StorageProcessor {
if (snapshotSize != null) {
newSnapshot.setPhysicalSize(snapshotSize);
}
} else if (primaryPool.getType() == StoragePoolType.CLVM) {
} else if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) {
CreateObjectAnswer result = takeClvmVolumeSnapshotOfStoppedVm(disk, snapshotName);
if (result != null) return result;
newSnapshot.setPath(snapshotPath);
@ -2999,24 +3018,24 @@ public class KVMStorageProcessor implements StorageProcessor {
if (snapshotTO.isKvmIncrementalSnapshot()) {
deleteCheckpoint(snapshotTO);
}
} else if (primaryPool.getType() == StoragePoolType.CLVM) {
// For CLVM, snapshots are typically already deleted from primary storage during backup
} else if (primaryPool.getType() == StoragePoolType.CLVM || primaryPool.getType() == StoragePoolType.CLVM_NG) {
// For CLVM/CLVM_NG, snapshots are typically already deleted from primary storage during backup
// via deleteSnapshotOnPrimary in the backupSnapshot finally block.
// This is called when the user explicitly deletes the snapshot via UI/API.
// We check if the snapshot still exists and clean it up if needed.
logger.info("Processing CLVM snapshot deletion (id={}, name={}, path={}) on primary storage",
logger.info("Processing CLVM/CLVM_NG snapshot deletion (id={}, name={}, path={}) on primary storage",
snapshotTO.getId(), snapshotTO.getName(), snapshotTO.getPath());
String snapshotPath = snapshotTO.getPath();
if (snapshotPath != null && !snapshotPath.isEmpty()) {
boolean wasDeleted = deleteClvmSnapshot(snapshotPath, true);
boolean wasDeleted = deleteClvmSnapshot(snapshotPath, primaryPool.getType(), true);
if (wasDeleted) {
logger.info("Successfully cleaned up CLVM snapshot {} from primary storage", snapshotName);
logger.info("Successfully cleaned up {} snapshot {} from primary storage", primaryPool.getType(), snapshotName);
} else {
logger.info("CLVM snapshot {} was already deleted from primary storage during backup, no cleanup needed", snapshotName);
logger.info("{} snapshot {} was already deleted from primary storage during backup, no cleanup needed", primaryPool.getType(), snapshotName);
}
} else {
logger.debug("CLVM snapshot path is null or empty, assuming already cleaned up");
logger.debug("{} snapshot path is null or empty, assuming already cleaned up", primaryPool.getType());
}
} else {
logger.warn("Operation not implemented for storage pool type of " + primaryPool.getType().toString());

View File

@ -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" || \

View File

@ -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;

View File

@ -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 {

View File

@ -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)

View File

@ -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));
}