feat(backup): restore path follows incremental backing-chain

Two changes that together let an incremental NAS backup be restored
without manual chain assembly:

  scripts/vm/hypervisor/kvm/nasbackup.sh
    - qemu-img rebase now writes a backing-file path that is RELATIVE to
      the new qcow2's directory (e.g. ../<parent-ts>/root.<uuid>.qcow2)
      rather than the absolute path on the current mount point. NAS mount
      points are ephemeral (mktemp -d), so an absolute reference would
      not resolve when the backup is re-mounted at restore time. Relative
      references are resolved by qemu-img against the file's own
      directory, so the chain stays valid no matter where the NAS is
      mounted next.
    - Verifies the parent file exists on the NAS before rebasing.

  LibvirtRestoreBackupCommandWrapper
    - For file-based primary storage (local, NFS-file), the existing
      code rsync'd the source qcow2 to the volume. That copies only the
      differential blocks of an incremental, leaving a volume whose
      backing-file reference points at a path the primary storage host
      doesn't have. Now: detect a backing-chain via qemu-img info JSON
      and flatten via 'qemu-img convert -O qcow2', which follows the
      chain and produces a self-contained qcow2. Full backups continue
      to use rsync (faster, no chain to flatten).
    - The block-storage path (RBD/Linstor) already used qemu-img convert
      via the QemuImg helper, which auto-flattens chains, so that path
      needed no change.

Refs: apache/cloudstack#12899
This commit is contained in:
James Peru 2026-04-27 19:18:33 +03:00
parent 43e2f7504a
commit 39303fbf88
2 changed files with 40 additions and 2 deletions

View File

@ -60,6 +60,15 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
private static final String ATTACH_RBD_DISK_XML_COMMAND = " virsh attach-device %s /dev/stdin <<EOF%sEOF";
private static final String CURRRENT_DEVICE = "virsh domblklist --domain %s | tail -n 3 | head -n 1 | awk '{print $1}'";
private static final String RSYNC_COMMAND = "rsync -az %s %s";
// Flattens the backing-file chain into a single self-contained qcow2 written to the
// destination volume path. Used when the source backup is an incremental whose qcow2
// has a backing reference to its parent (chain set up by nasbackup.sh's qemu-img rebase).
private static final String QEMU_IMG_FLATTEN_COMMAND = "qemu-img convert -O qcow2 %s %s";
// Detects whether a qcow2 file references a parent in its backing-file metadata.
// Returns 0 (true) when a backing file is present, 1 when not. Uses --output=json
// so the test is robust to qemu-img version differences in human-readable output.
private static final String QEMU_IMG_HAS_BACKING_COMMAND =
"qemu-img info --output=json %s 2>/dev/null | grep -q '\"backing-filename\"'";
private String getVolumeUuidFromPath(String volumePath, PrimaryDataStoreTO volumePool) {
if (Storage.StoragePoolType.Linstor.equals(volumePool.getPoolType())) {
@ -270,10 +279,27 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
return replaceBlockDeviceWithBackup(storagePoolMgr, volumePool, volumePath, backupPath, timeout, createTargetVolume, size);
}
// For NAS-backed incremental backups, the source qcow2 has a backing-file
// reference to its parent (set by nasbackup.sh's qemu-img rebase). A plain
// rsync would copy only the differential blocks, leaving a volume that
// depends on a backing file the primary storage doesn't have. Flatten the
// chain via qemu-img convert, which follows the backing-file links and
// produces a single self-contained qcow2.
if (hasBackingChain(backupPath)) {
int flattenExit = Script.runSimpleBashScriptForExitValue(
String.format(QEMU_IMG_FLATTEN_COMMAND, backupPath, volumePath), timeout, false);
return flattenExit == 0;
}
int exitValue = Script.runSimpleBashScriptForExitValue(String.format(RSYNC_COMMAND, backupPath, volumePath), timeout, false);
return exitValue == 0;
}
private boolean hasBackingChain(String qcow2Path) {
return Script.runSimpleBashScriptForExitValue(
String.format(QEMU_IMG_HAS_BACKING_COMMAND, qcow2Path)) == 0;
}
private boolean replaceBlockDeviceWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout, boolean createTargetVolume, Long size) {
KVMStoragePool volumeStoragePool = storagePoolMgr.getStoragePool(volumePool.getPoolType(), volumePool.getUuid());
QemuImg qemu;

View File

@ -268,8 +268,20 @@ XML
if [[ "$fullpath" == /dev/drbd/by-res/* ]]; then
volUuid=$(get_linstor_uuid_from_path "$fullpath")
fi
if ! qemu-img rebase -u -b "$PARENT_PATH" -F qcow2 "$dest/$name.$volUuid.qcow2" >> "$logFile" 2> >(cat >&2); then
echo "qemu-img rebase failed for $dest/$name.$volUuid.qcow2 onto $PARENT_PATH"
# PARENT_PATH from the orchestrator is the parent backup's path relative to the
# NAS mount root (e.g. "i-2-X/2026.04.27.12.00.00/root.UUID.qcow2"). Convert it to
# a path relative to THIS new qcow2's directory so the backing reference resolves
# correctly the next time the NAS is mounted (mount points are ephemeral).
local parent_abs="$mount_point/$PARENT_PATH"
if [[ ! -f "$parent_abs" ]]; then
echo "Parent backup file does not exist on NAS: $parent_abs"
cleanup
exit 1
fi
local parent_rel
parent_rel=$(realpath --relative-to="$dest" "$parent_abs")
if ! qemu-img rebase -u -b "$parent_rel" -F qcow2 "$dest/$name.$volUuid.qcow2" >> "$logFile" 2> >(cat >&2); then
echo "qemu-img rebase failed for $dest/$name.$volUuid.qcow2 onto $parent_rel"
cleanup
exit 1
fi