Skip to content

Commit 39303fb

Browse files
committed
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: #12899
1 parent 43e2f75 commit 39303fb

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBa
6060
private static final String ATTACH_RBD_DISK_XML_COMMAND = " virsh attach-device %s /dev/stdin <<EOF%sEOF";
6161
private static final String CURRRENT_DEVICE = "virsh domblklist --domain %s | tail -n 3 | head -n 1 | awk '{print $1}'";
6262
private static final String RSYNC_COMMAND = "rsync -az %s %s";
63+
// Flattens the backing-file chain into a single self-contained qcow2 written to the
64+
// destination volume path. Used when the source backup is an incremental whose qcow2
65+
// has a backing reference to its parent (chain set up by nasbackup.sh's qemu-img rebase).
66+
private static final String QEMU_IMG_FLATTEN_COMMAND = "qemu-img convert -O qcow2 %s %s";
67+
// Detects whether a qcow2 file references a parent in its backing-file metadata.
68+
// Returns 0 (true) when a backing file is present, 1 when not. Uses --output=json
69+
// so the test is robust to qemu-img version differences in human-readable output.
70+
private static final String QEMU_IMG_HAS_BACKING_COMMAND =
71+
"qemu-img info --output=json %s 2>/dev/null | grep -q '\"backing-filename\"'";
6372

6473
private String getVolumeUuidFromPath(String volumePath, PrimaryDataStoreTO volumePool) {
6574
if (Storage.StoragePoolType.Linstor.equals(volumePool.getPoolType())) {
@@ -270,10 +279,27 @@ private boolean replaceVolumeWithBackup(KVMStoragePoolManager storagePoolMgr, Pr
270279
return replaceBlockDeviceWithBackup(storagePoolMgr, volumePool, volumePath, backupPath, timeout, createTargetVolume, size);
271280
}
272281

282+
// For NAS-backed incremental backups, the source qcow2 has a backing-file
283+
// reference to its parent (set by nasbackup.sh's qemu-img rebase). A plain
284+
// rsync would copy only the differential blocks, leaving a volume that
285+
// depends on a backing file the primary storage doesn't have. Flatten the
286+
// chain via qemu-img convert, which follows the backing-file links and
287+
// produces a single self-contained qcow2.
288+
if (hasBackingChain(backupPath)) {
289+
int flattenExit = Script.runSimpleBashScriptForExitValue(
290+
String.format(QEMU_IMG_FLATTEN_COMMAND, backupPath, volumePath), timeout, false);
291+
return flattenExit == 0;
292+
}
293+
273294
int exitValue = Script.runSimpleBashScriptForExitValue(String.format(RSYNC_COMMAND, backupPath, volumePath), timeout, false);
274295
return exitValue == 0;
275296
}
276297

298+
private boolean hasBackingChain(String qcow2Path) {
299+
return Script.runSimpleBashScriptForExitValue(
300+
String.format(QEMU_IMG_HAS_BACKING_COMMAND, qcow2Path)) == 0;
301+
}
302+
277303
private boolean replaceBlockDeviceWithBackup(KVMStoragePoolManager storagePoolMgr, PrimaryDataStoreTO volumePool, String volumePath, String backupPath, int timeout, boolean createTargetVolume, Long size) {
278304
KVMStoragePool volumeStoragePool = storagePoolMgr.getStoragePool(volumePool.getPoolType(), volumePool.getUuid());
279305
QemuImg qemu;

scripts/vm/hypervisor/kvm/nasbackup.sh

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,20 @@ XML
268268
if [[ "$fullpath" == /dev/drbd/by-res/* ]]; then
269269
volUuid=$(get_linstor_uuid_from_path "$fullpath")
270270
fi
271-
if ! qemu-img rebase -u -b "$PARENT_PATH" -F qcow2 "$dest/$name.$volUuid.qcow2" >> "$logFile" 2> >(cat >&2); then
272-
echo "qemu-img rebase failed for $dest/$name.$volUuid.qcow2 onto $PARENT_PATH"
271+
# PARENT_PATH from the orchestrator is the parent backup's path relative to the
272+
# NAS mount root (e.g. "i-2-X/2026.04.27.12.00.00/root.UUID.qcow2"). Convert it to
273+
# a path relative to THIS new qcow2's directory so the backing reference resolves
274+
# correctly the next time the NAS is mounted (mount points are ephemeral).
275+
local parent_abs="$mount_point/$PARENT_PATH"
276+
if [[ ! -f "$parent_abs" ]]; then
277+
echo "Parent backup file does not exist on NAS: $parent_abs"
278+
cleanup
279+
exit 1
280+
fi
281+
local parent_rel
282+
parent_rel=$(realpath --relative-to="$dest" "$parent_abs")
283+
if ! qemu-img rebase -u -b "$parent_rel" -F qcow2 "$dest/$name.$volUuid.qcow2" >> "$logFile" 2> >(cat >&2); then
284+
echo "qemu-img rebase failed for $dest/$name.$volUuid.qcow2 onto $parent_rel"
273285
cleanup
274286
exit 1
275287
fi

0 commit comments

Comments
 (0)