Skip to content

Commit fbb916b

Browse files
committed
feat(backup): nasbackup.sh full+incremental modes via backup-begin
Adds three new optional CLI flags to nasbackup.sh: -M|--mode <full|incremental> --bitmap-new <name> (checkpoint to create with this backup) --bitmap-parent <name> (incremental: parent bitmap to read changes since) --parent-path <path> (incremental: parent backup file for rebase) Behavior: - When -M is omitted, behavior is unchanged (legacy full-only, no checkpoint created), so existing callers are not affected. - With -M full + --bitmap-new, a full backup is taken AND a libvirt checkpoint of that name is registered atomically (via backup-begin's --checkpointxml), giving the next incremental its starting bitmap. - With -M incremental, libvirt's <incremental> element references the parent bitmap; only changed blocks are written. After completion, qemu-img rebase wires the new file to its parent so the chain on the NAS is self-describing for restore. - Stopped VMs cannot use backup-begin; if -M incremental is requested while VM is stopped, the script falls back to a full and emits INCREMENTAL_FALLBACK= on stderr so the orchestrator can record it correctly in the chain. - The script echoes BITMAP_CREATED=<name> on success so the Java caller can store it under backup_details (NASBackupChainKeys.BITMAP_NAME). Works across local file, NFS-file, and LINSTOR primary storage. Ceph RBD running-VM support is a pre-existing limitation of this script, not affected by this change. Refs: #12899
1 parent 1981469 commit fbb916b

1 file changed

Lines changed: 124 additions & 7 deletions

File tree

scripts/vm/hypervisor/kvm/nasbackup.sh

Lines changed: 124 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,15 @@ MOUNT_OPTS=""
3333
BACKUP_DIR=""
3434
DISK_PATHS=""
3535
QUIESCE=""
36+
# Incremental backup parameters (all optional; legacy callers omit them)
37+
MODE="" # "full" or "incremental"; empty => legacy full-only behavior (no checkpoint created)
38+
BITMAP_NEW="" # Bitmap/checkpoint name to create with this backup (e.g. "backup-1711586400")
39+
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)
3641
logFile="/var/log/cloudstack/agent/agent.log"
3742

3843
EXIT_CLEANUP_FAILED=20
44+
EXIT_INCREMENTAL_UNSUPPORTED=21
3945

4046
log() {
4147
[[ "$verb" -eq 1 ]] && builtin echo "$@"
@@ -113,20 +119,70 @@ backup_running_vm() {
113119
mount_operation
114120
mkdir -p "$dest" || { echo "Failed to create backup directory $dest"; exit 1; }
115121

122+
# Determine effective mode for this run.
123+
# Legacy callers (no -M argument) get the original full-only behavior with no checkpoint.
124+
local effective_mode="${MODE:-legacy-full}"
125+
local make_checkpoint=0
126+
case "$effective_mode" in
127+
incremental)
128+
if [[ -z "$BITMAP_PARENT" || -z "$BITMAP_NEW" || -z "$PARENT_PATH" ]]; then
129+
echo "incremental mode requires --bitmap-parent, --bitmap-new, and --parent-path"
130+
cleanup
131+
exit 1
132+
fi
133+
make_checkpoint=1
134+
;;
135+
full)
136+
if [[ -z "$BITMAP_NEW" ]]; then
137+
echo "full mode requires --bitmap-new (the bitmap to create for the next incremental)"
138+
cleanup
139+
exit 1
140+
fi
141+
make_checkpoint=1
142+
;;
143+
legacy-full)
144+
make_checkpoint=0
145+
;;
146+
*)
147+
echo "Unknown mode: $effective_mode"
148+
cleanup
149+
exit 1
150+
;;
151+
esac
152+
153+
# Build backup XML (and matching checkpoint XML when applicable).
116154
name="root"
117-
echo "<domainbackup mode='push'><disks>" > $dest/backup.xml
155+
echo "<domainbackup mode='push'>" > $dest/backup.xml
156+
if [[ "$effective_mode" == "incremental" ]]; then
157+
echo "<incremental>$BITMAP_PARENT</incremental>" >> $dest/backup.xml
158+
fi
159+
echo "<disks>" >> $dest/backup.xml
160+
if [[ $make_checkpoint -eq 1 ]]; then
161+
echo "<domaincheckpoint><name>$BITMAP_NEW</name><disks>" > $dest/checkpoint.xml
162+
fi
118163
while read -r disk fullpath; do
119164
if [[ "$fullpath" == /dev/drbd/by-res/* ]]; then
120165
volUuid=$(get_linstor_uuid_from_path "$fullpath")
121166
else
122167
volUuid="${fullpath##*/}"
123168
fi
124-
echo "<disk name='$disk' backup='yes' type='file' backupmode='full'><driver type='qcow2'/><target file='$dest/$name.$volUuid.qcow2' /></disk>" >> $dest/backup.xml
169+
if [[ "$effective_mode" == "incremental" ]]; then
170+
# Incremental disk entry — no backupmode attr, libvirt picks it up from <incremental>.
171+
echo "<disk name='$disk' backup='yes' type='file'><driver type='qcow2'/><target file='$dest/$name.$volUuid.qcow2' /></disk>" >> $dest/backup.xml
172+
else
173+
echo "<disk name='$disk' backup='yes' type='file' backupmode='full'><driver type='qcow2'/><target file='$dest/$name.$volUuid.qcow2' /></disk>" >> $dest/backup.xml
174+
fi
175+
if [[ $make_checkpoint -eq 1 ]]; then
176+
echo "<disk name='$disk'/>" >> $dest/checkpoint.xml
177+
fi
125178
name="datadisk"
126179
done < <(
127180
virsh -c qemu:///system domblklist "$VM" --details 2>/dev/null | awk '$2=="disk"{print $3, $4}'
128181
)
129182
echo "</disks></domainbackup>" >> $dest/backup.xml
183+
if [[ $make_checkpoint -eq 1 ]]; then
184+
echo "</disks></domaincheckpoint>" >> $dest/checkpoint.xml
185+
fi
130186

131187
local thaw=0
132188
if [[ ${QUIESCE} == "true" ]]; then
@@ -135,10 +191,16 @@ backup_running_vm() {
135191
fi
136192
fi
137193

138-
# Start push backup
194+
# Start push backup, atomically registering the new checkpoint when applicable.
139195
local backup_begin=0
140-
if virsh -c qemu:///system backup-begin --domain $VM --backupxml $dest/backup.xml 2>&1 > /dev/null; then
141-
backup_begin=1;
196+
if [[ $make_checkpoint -eq 1 ]]; then
197+
if virsh -c qemu:///system backup-begin --domain $VM --backupxml $dest/backup.xml --checkpointxml $dest/checkpoint.xml 2>&1 > /dev/null; then
198+
backup_begin=1;
199+
fi
200+
else
201+
if virsh -c qemu:///system backup-begin --domain $VM --backupxml $dest/backup.xml 2>&1 > /dev/null; then
202+
backup_begin=1;
203+
fi
142204
fi
143205

144206
if [[ $thaw -eq 1 ]]; then
@@ -172,9 +234,25 @@ backup_running_vm() {
172234
sleep 5
173235
done
174236

175-
# Use qemu-img convert to sparsify linstor backups which get bloated due to virsh backup-begin.
237+
# Sparsify behavior:
238+
# - For LINSTOR backups (existing): qemu-img convert sparsifies the bloated output.
239+
# - For INCREMENTAL: rebase the resulting thin qcow2 onto its parent so the chain is self-describing
240+
# (so a future restore can flatten without external chain metadata).
176241
name="root"
177242
while read -r disk fullpath; do
243+
if [[ "$effective_mode" == "incremental" ]]; then
244+
volUuid="${fullpath##*/}"
245+
if [[ "$fullpath" == /dev/drbd/by-res/* ]]; then
246+
volUuid=$(get_linstor_uuid_from_path "$fullpath")
247+
fi
248+
if ! qemu-img rebase -u -b "$PARENT_PATH" -F qcow2 "$dest/$name.$volUuid.qcow2" >> "$logFile" 2> >(cat >&2); then
249+
echo "qemu-img rebase failed for $dest/$name.$volUuid.qcow2 onto $PARENT_PATH"
250+
cleanup
251+
exit 1
252+
fi
253+
name="datadisk"
254+
continue
255+
fi
178256
if [[ "$fullpath" != /dev/drbd/by-res/* ]]; then
179257
continue
180258
fi
@@ -191,18 +269,30 @@ backup_running_vm() {
191269
virsh -c qemu:///system domblklist "$VM" --details 2>/dev/null | awk '$2=="disk"{print $3, $4}'
192270
)
193271

194-
rm -f $dest/backup.xml
272+
rm -f $dest/backup.xml $dest/checkpoint.xml
195273
sync
196274

197275
# Print statistics
198276
virsh -c qemu:///system domjobinfo $VM --completed
199277
du -sb $dest | cut -f1
278+
if [[ -n "$BITMAP_NEW" ]]; then
279+
# Echo the bitmap name on its own line so the Java caller can capture it for backup_details.
280+
echo "BITMAP_CREATED=$BITMAP_NEW"
281+
fi
200282

201283
umount $mount_point
202284
rmdir $mount_point
203285
}
204286

205287
backup_stopped_vm() {
288+
# Stopped VMs cannot use libvirt's backup-begin (no QEMU process). Take a full
289+
# backup via qemu-img convert. If the caller asked for incremental, fall back
290+
# to full and signal the fallback so the orchestrator can record it as a full
291+
# in the chain.
292+
if [[ "$MODE" == "incremental" ]]; then
293+
echo "INCREMENTAL_FALLBACK=full (VM stopped — incremental requires running VM)" >&2
294+
fi
295+
206296
mount_operation
207297
mkdir -p "$dest" || { echo "Failed to create backup directory $dest"; exit 1; }
208298

@@ -278,6 +368,13 @@ cleanup() {
278368
function usage {
279369
echo ""
280370
echo "Usage: $0 -o <operation> -v|--vm <domain name> -t <storage type> -s <storage address> -m <mount options> -p <backup path> -d <disks path> -q|--quiesce <true|false>"
371+
echo " [-M|--mode <full|incremental>] [--bitmap-new <name>] [--bitmap-parent <name>] [--parent-path <path>]"
372+
echo ""
373+
echo "Incremental backup options (running VMs only; requires QEMU >= 4.2 and libvirt >= 7.2):"
374+
echo " -M|--mode full Take a full backup AND create a checkpoint (--bitmap-new required) for future incrementals."
375+
echo " -M|--mode incremental Take an incremental backup since --bitmap-parent and create new checkpoint --bitmap-new."
376+
echo " Requires --bitmap-parent, --bitmap-new, and --parent-path (parent backup file for rebase)."
377+
echo " Without -M, behaves as legacy full-only backup with no checkpoint creation."
281378
echo ""
282379
exit 1
283380
}
@@ -324,6 +421,26 @@ while [[ $# -gt 0 ]]; do
324421
shift
325422
shift
326423
;;
424+
-M|--mode)
425+
MODE="$2"
426+
shift
427+
shift
428+
;;
429+
--bitmap-new)
430+
BITMAP_NEW="$2"
431+
shift
432+
shift
433+
;;
434+
--bitmap-parent)
435+
BITMAP_PARENT="$2"
436+
shift
437+
shift
438+
;;
439+
--parent-path)
440+
PARENT_PATH="$2"
441+
shift
442+
shift
443+
;;
327444
-h|--help)
328445
usage
329446
shift

0 commit comments

Comments
 (0)