Skip to content

Commit 5be1910

Browse files
committed
refactor(backup): per-volume parent paths for incremental NAS backup
Address abh1sar's review at NASBackupProvider.java:340 — backup files on the NAS are named after each volume's own UUID (root.<uuid>.qcow2 and datadisk.<uuid>.qcow2), so a single parentPath shared across every disk in the loop would have rebased every data disk's new qcow2 onto the *root* parent file. Mirroring how RestoreBackupCommand passes a list of per-volume backup files, TakeBackupCommand now carries a parentPaths list (one per VM volume in deviceId order) and the script rebases each disk against the matching entry. Wire changes: * TakeBackupCommand: replace parentPath:String with parentPaths:List<String> * LibvirtTakeBackupCommandWrapper: pass --parent-paths <csv> (was --parent-path) * nasbackup.sh: parse --parent-paths into an array, index by disk position in the rebase loop, fail loudly if the list is shorter than DISK_PATHS * NASBackupProvider: new composeParentBackupPaths(parent, vmId) reads the parent's backedUpVolumes list (uuid order = deviceId order at parent time) and emits the correctly-named file per position. Returns null when the current VM's volume count differs from the parent — decideChain then falls back to a fresh full rather than risk a misaligned rebase. * ChainDecision: parentPath:String -> parentPaths:List<String> All 14 unit tests still pass. The bash script's --parent-path arg is renamed to --parent-paths (no orchestrator outside this PR ever populated the old single-path arg, so no compatibility shim needed).
1 parent 0bdcdb1 commit 5be1910

4 files changed

Lines changed: 112 additions & 42 deletions

File tree

core/src/main/java/org/apache/cloudstack/backup/TakeBackupCommand.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ public class TakeBackupCommand extends Command {
4040
private String mode; // "full" or "incremental"; null => legacy behaviour (script default)
4141
private String bitmapNew; // Checkpoint/bitmap name to create with this backup (timestamp-based)
4242
private String bitmapParent; // Incremental: parent bitmap to read changes since
43-
private String parentPath; // Incremental: parent backup file path on the mounted NAS (for qemu-img rebase)
43+
44+
// Per-volume parent backup file paths (one per VM volume, ordered by deviceId — same
45+
// order as volumePaths). The script rebases each new qcow2 onto the matching parent.
46+
// Addresses abh1sar review at NASBackupProvider.java:340 — backup file UUIDs differ
47+
// across volumes, so a single parentPath would have rebased every data disk onto the
48+
// root file. New callers MUST populate parentPaths.
49+
private List<String> parentPaths;
4450

4551
public TakeBackupCommand(String vmName, String backupPath) {
4652
super();
@@ -136,12 +142,12 @@ public void setBitmapParent(String bitmapParent) {
136142
this.bitmapParent = bitmapParent;
137143
}
138144

139-
public String getParentPath() {
140-
return parentPath;
145+
public List<String> getParentPaths() {
146+
return parentPaths;
141147
}
142148

143-
public void setParentPath(String parentPath) {
144-
this.parentPath = parentPath;
149+
public void setParentPaths(List<String> parentPaths) {
150+
this.parentPaths = parentPaths;
145151
}
146152

147153
@Override

plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -204,19 +204,23 @@ protected Host getVMHypervisorHost(VirtualMachine vm) {
204204
* parent bitmap and parent file path.
205205
*/
206206
static final class ChainDecision {
207-
final String mode; // "full" or "incremental"
207+
final String mode; // "full" or "incremental"
208208
final String bitmapNew;
209-
final String bitmapParent; // null for full
210-
final String parentPath; // null for full
211-
final String chainId; // chain identifier this backup belongs to
212-
final int chainPosition; // 0 for full, N for the Nth incremental in the chain
213-
214-
private ChainDecision(String mode, String bitmapNew, String bitmapParent, String parentPath,
209+
final String bitmapParent; // null for full
210+
// Per-volume parent backup file paths, one per current VM volume in deviceId order.
211+
// null/empty for full. Replaces the single parentPath field — each volume needs its
212+
// own parent file because backup files are named after each volume's own UUID
213+
// (root.<uuid>.qcow2 / datadisk.<uuid>.qcow2), abh1sar review at line 340.
214+
final List<String> parentPaths;
215+
final String chainId; // chain identifier this backup belongs to
216+
final int chainPosition; // 0 for full, N for the Nth incremental in the chain
217+
218+
private ChainDecision(String mode, String bitmapNew, String bitmapParent, List<String> parentPaths,
215219
String chainId, int chainPosition) {
216220
this.mode = mode;
217221
this.bitmapNew = bitmapNew;
218222
this.bitmapParent = bitmapParent;
219-
this.parentPath = parentPath;
223+
this.parentPaths = parentPaths;
220224
this.chainId = chainId;
221225
this.chainPosition = chainPosition;
222226
}
@@ -226,10 +230,10 @@ static ChainDecision fullStart(String bitmapName) {
226230
UUID.randomUUID().toString(), 0);
227231
}
228232

229-
static ChainDecision incremental(String bitmapNew, String bitmapParent, String parentPath,
233+
static ChainDecision incremental(String bitmapNew, String bitmapParent, List<String> parentPaths,
230234
String chainId, int chainPosition) {
231235
return new ChainDecision(NASBackupChainKeys.TYPE_INCREMENTAL, bitmapNew, bitmapParent,
232-
parentPath, chainId, chainPosition);
236+
parentPaths, chainId, chainPosition);
233237
}
234238

235239
boolean isIncremental() {
@@ -305,11 +309,18 @@ protected ChainDecision decideChain(VirtualMachine vm) {
305309
return ChainDecision.fullStart(newBitmap);
306310
}
307311

308-
// The script needs the parent backup's on-NAS file path so it can rebase the new
309-
// qcow2 onto it. The path is stored relative to the NAS mount point — the script
310-
// resolves it inside its mount session.
311-
String parentPath = composeParentBackupPath(parent);
312-
return ChainDecision.incremental(newBitmap, activeCheckpoint, parentPath,
312+
// The script needs the parent backup's on-NAS file path PER VOLUME so it can rebase
313+
// each new qcow2 onto the matching parent. The paths are stored relative to the NAS
314+
// mount root — the script resolves them inside its mount session. When alignment
315+
// fails (volume count changed, etc.) compose returns null and we fall back to full
316+
// so we don't risk corrupting the chain.
317+
List<String> parentPaths = composeParentBackupPaths(parent, vm.getId());
318+
if (parentPaths == null) {
319+
LOG.debug("VM {} parent backup {} volume layout no longer matches current VM — forcing full",
320+
vm.getInstanceName(), parent.getUuid());
321+
return ChainDecision.fullStart(newBitmap);
322+
}
323+
return ChainDecision.incremental(newBitmap, activeCheckpoint, parentPaths,
313324
parentChainId, parentChainPosition + 1);
314325
}
315326

@@ -353,15 +364,51 @@ private String readDetail(Backup backup, String key) {
353364
}
354365

355366
/**
356-
* Compose the on-NAS path of a parent backup's root-disk qcow2. Relative to the NAS mount,
357-
* matches the layout written by {@code nasbackup.sh} ({@code <backupPath>/root.<volUuid>.qcow2}).
367+
* Compose the on-NAS path of EVERY parent backup file (one per VM volume) in the same
368+
* order the script will iterate the current VM's disks (deviceId asc). Relative to the
369+
* NAS mount, matches the layout written by {@code nasbackup.sh}:
370+
* first disk -> {@code <backupPath>/root.<volUuid>.qcow2}
371+
* others -> {@code <backupPath>/datadisk.<volUuid>.qcow2}
372+
*
373+
* Returns {@code null} if the parent's stored volume list can't be aligned with the
374+
* current VM's volumes (count mismatch / different volume identities). In that case the
375+
* caller should force a fresh full so we don't accidentally rebase a data disk onto the
376+
* root parent — exactly the bug abh1sar flagged at line 340.
358377
*/
359-
private String composeParentBackupPath(Backup parent) {
360-
// backupPath is stored as externalId by createBackupObject — e.g. "i-2-1234-VM/2026.04.27.13.45.00".
361-
// Volume UUID for the root volume is what the script keys backup files on.
362-
VolumeVO rootVolume = volumeDao.getInstanceRootVolume(parent.getVmId());
363-
String volUuid = rootVolume == null ? "root" : rootVolume.getUuid();
364-
return parent.getExternalId() + "/root." + volUuid + ".qcow2";
378+
private List<String> composeParentBackupPaths(Backup parent, long vmId) {
379+
// backupPath is stored as externalId by createBackupObject — e.g.
380+
// "i-2-1234-VM/2026.04.27.13.45.00".
381+
String dir = parent.getExternalId();
382+
if (dir == null || dir.isEmpty()) {
383+
return null;
384+
}
385+
386+
// Read the parent's backed-up volume list (uuid order = deviceId order at the time
387+
// the parent was taken). The script names files as root.<uuid>.qcow2 for the first
388+
// entry and datadisk.<uuid>.qcow2 for subsequent entries — match that here.
389+
List<Backup.VolumeInfo> parentVols = parent.getBackedUpVolumes();
390+
if (parentVols == null || parentVols.isEmpty()) {
391+
return null;
392+
}
393+
394+
// Sanity: the current VM must have the same number of volumes. If it doesn't (volume
395+
// added or removed since the parent), positional alignment is unsafe — fall back to
396+
// full at the caller.
397+
List<VolumeVO> currentVols = volumeDao.findByInstance(vmId);
398+
if (currentVols == null || currentVols.size() != parentVols.size()) {
399+
return null;
400+
}
401+
402+
List<String> paths = new ArrayList<>(parentVols.size());
403+
for (int i = 0; i < parentVols.size(); i++) {
404+
String volUuid = parentVols.get(i).getUuid();
405+
if (volUuid == null || volUuid.isEmpty()) {
406+
return null;
407+
}
408+
String prefix = (i == 0) ? "root" : "datadisk";
409+
paths.add(dir + "/" + prefix + "." + volUuid + ".qcow2");
410+
}
411+
return paths;
365412
}
366413

367414
/**
@@ -479,7 +526,7 @@ public Pair<Boolean, Backup> takeBackup(final VirtualMachine vm, Boolean quiesce
479526
command.setMode(decision.mode);
480527
command.setBitmapNew(decision.bitmapNew);
481528
command.setBitmapParent(decision.bitmapParent);
482-
command.setParentPath(decision.parentPath);
529+
command.setParentPaths(decision.parentPaths);
483530

484531
if (VirtualMachine.State.Stopped.equals(vm.getState())) {
485532
List<VolumeVO> vmVolumes = volumeDao.findByInstance(vm.getId());

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,9 @@ public Answer execute(TakeBackupCommand command, LibvirtComputingResource libvir
9393
argv.add("--bitmap-parent");
9494
argv.add(command.getBitmapParent());
9595
}
96-
if (command.getParentPath() != null && !command.getParentPath().isEmpty()) {
97-
argv.add("--parent-path");
98-
argv.add(command.getParentPath());
96+
if (command.getParentPaths() != null && !command.getParentPaths().isEmpty()) {
97+
argv.add("--parent-paths");
98+
argv.add(String.join(",", command.getParentPaths()));
9999
}
100100

101101
List<String[]> commands = new ArrayList<>();

scripts/vm/hypervisor/kvm/nasbackup.sh

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ QUIESCE=""
3737
MODE="" # "full" or "incremental"; empty => legacy full-only behavior (no checkpoint created)
3838
BITMAP_NEW="" # Bitmap/checkpoint name to create with this backup (e.g. "backup-1711586400")
3939
BITMAP_PARENT="" # For incremental: parent bitmap name to read changes since
40-
PARENT_PATH="" # For incremental: parent backup file path (used as backing for qemu-img rebase)
40+
PARENT_PATHS="" # For incremental: comma-separated list of parent backup file paths,
41+
# one per VM volume in the same order as DISK_PATHS. Each new qcow2
42+
# is rebased onto its corresponding parent file. Required because
43+
# data-disk backup files don't share the root volume's UUID — abh1sar
44+
# review at NASBackupProvider.java:340.
4145
# Rebase operation parameters (used only with -o rebase, for chain repair on delete-middle)
4246
REBASE_TARGET="" # The qcow2 file to repoint at a new backing (mount-relative path)
4347
REBASE_NEW_BACKING="" # The new backing parent file (mount-relative path)
@@ -128,8 +132,8 @@ backup_running_vm() {
128132
local make_checkpoint=0
129133
case "$effective_mode" in
130134
incremental)
131-
if [[ -z "$BITMAP_PARENT" || -z "$BITMAP_NEW" || -z "$PARENT_PATH" ]]; then
132-
echo "incremental mode requires --bitmap-parent, --bitmap-new, and --parent-path"
135+
if [[ -z "$BITMAP_PARENT" || -z "$BITMAP_NEW" || -z "$PARENT_PATHS" ]]; then
136+
echo "incremental mode requires --bitmap-parent, --bitmap-new, and --parent-paths"
133137
cleanup
134138
exit 1
135139
fi
@@ -278,17 +282,29 @@ XML
278282
# - For INCREMENTAL: rebase the resulting thin qcow2 onto its parent so the chain is self-describing
279283
# (so a future restore can flatten without external chain metadata).
280284
name="root"
285+
# PARENT_PATHS arrives as a comma-separated list, one entry per VM volume in the same
286+
# order as DISK_PATHS. Split into a bash array so we can index by disk position.
287+
local -a parent_paths_arr=()
288+
if [[ "$effective_mode" == "incremental" && -n "$PARENT_PATHS" ]]; then
289+
IFS=',' read -ra parent_paths_arr <<< "$PARENT_PATHS"
290+
fi
291+
local disk_idx=0
281292
while read -r disk fullpath; do
282293
if [[ "$effective_mode" == "incremental" ]]; then
283294
volUuid="${fullpath##*/}"
284295
if [[ "$fullpath" == /dev/drbd/by-res/* ]]; then
285296
volUuid=$(get_linstor_uuid_from_path "$fullpath")
286297
fi
287-
# PARENT_PATH from the orchestrator is the parent backup's path relative to the
288-
# NAS mount root (e.g. "i-2-X/2026.04.27.12.00.00/root.UUID.qcow2"). Convert it to
289-
# a path relative to THIS new qcow2's directory so the backing reference resolves
290-
# correctly the next time the NAS is mounted (mount points are ephemeral).
291-
local parent_abs="$mount_point/$PARENT_PATH"
298+
# Pick this disk's specific parent file. Each volume's backup is named after its
299+
# own UUID so a single PARENT_PATH would rebase data disks onto the root parent —
300+
# exactly the bug abh1sar called out at NASBackupProvider.java:340.
301+
if [[ $disk_idx -ge ${#parent_paths_arr[@]} ]]; then
302+
echo "PARENT_PATHS list shorter than DISK_PATHS — missing parent for disk index $disk_idx"
303+
cleanup
304+
exit 1
305+
fi
306+
local this_parent_rel="${parent_paths_arr[$disk_idx]}"
307+
local parent_abs="$mount_point/$this_parent_rel"
292308
if [[ ! -f "$parent_abs" ]]; then
293309
echo "Parent backup file does not exist on NAS: $parent_abs"
294310
cleanup
@@ -302,6 +318,7 @@ XML
302318
exit 1
303319
fi
304320
name="datadisk"
321+
disk_idx=$((disk_idx + 1))
305322
continue
306323
fi
307324
if [[ "$fullpath" != /dev/drbd/by-res/* ]]; then
@@ -556,8 +573,8 @@ while [[ $# -gt 0 ]]; do
556573
shift
557574
shift
558575
;;
559-
--parent-path)
560-
PARENT_PATH="$2"
576+
--parent-paths)
577+
PARENT_PATHS="$2"
561578
shift
562579
shift
563580
;;

0 commit comments

Comments
 (0)