@@ -33,9 +33,15 @@ MOUNT_OPTS=""
3333BACKUP_DIR=" "
3434DISK_PATHS=" "
3535QUIESCE=" "
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)
3641logFile=" /var/log/cloudstack/agent/agent.log"
3742
3843EXIT_CLEANUP_FAILED=20
44+ EXIT_INCREMENTAL_UNSUPPORTED=21
3945
4046log () {
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
205287backup_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() {
278368function 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