Skip to content

Commit 3ed8a30

Browse files
committed
backup(nas): regression test — incremental backup survives a VM restart
Automates the manual libvirt-host validation that caught the parent-checkpoint recreation bug: FULL + marker -> stop/start the VM (wipes libvirt's checkpoint registry) -> INCREMENTAL + marker -> restore the tip -> assert both markers are present and the post-restart backup is INCREMENTAL (not silently a FULL). Preserves the original nas.backup.full.every in finally. Guards #13074.
1 parent 8adde4d commit 3ed8a30

1 file changed

Lines changed: 84 additions & 0 deletions

File tree

test/integration/smoke/test_backup_recovery_nas.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,90 @@ def test_incremental_chain_cadence(self):
334334
self._set_full_every(original_full_every)
335335
self.backup_offering.removeOffering(self.apiclient, self.vm.id)
336336

337+
@attr(tags=["advanced", "backup"], required_hardware="true")
338+
def test_incremental_after_vm_restart(self):
339+
"""
340+
Regression for the parent-checkpoint recreation bug (PR #13074): an incremental
341+
backup taken AFTER the VM has been restarted must still succeed and restore
342+
correctly.
343+
344+
A VM (re)start rebuilds the libvirt domain XML and wipes libvirt's checkpoint
345+
registry, while the dirty bitmap persists on the qcow2. The agent must then
346+
re-register the parent checkpoint with `checkpoint-create --redefine` (from the
347+
saved checkpoint XML) rather than a fresh create — a fresh create fails with
348+
"Bitmap already exists", and qemu-img cannot drop the bitmap on a running disk.
349+
350+
How this was reproduced manually on a libvirt 10.0.0 host, and what this test
351+
automates:
352+
FULL + marker1 -> stop/start the VM (wipes the checkpoint registry)
353+
-> INCREMENTAL + marker2 -> restore the tip -> both markers present.
354+
"""
355+
self.backup_offering.assignOffering(self.apiclient, self.vm.id)
356+
original_full_every = self._get_full_every()
357+
# High cadence so the post-restart backup is INCREMENTAL, not a periodic FULL.
358+
self._set_full_every(100)
359+
backups = []
360+
try:
361+
ssh_client_vm = self.vm.get_ssh_client(reconnect=True)
362+
ssh_client_vm.execute("echo restart-test-1 > /root/restart_marker_1.txt; sync")
363+
364+
# 1) FULL anchor
365+
Backup.create(self.apiclient, self.vm.id, "restart_full")
366+
time.sleep(2)
367+
368+
# 2) Restart the VM — wipes libvirt's checkpoint registry (the bug trigger).
369+
self.vm.stop(self.apiclient)
370+
self.vm.start(self.apiclient)
371+
ssh_client_vm = self.vm.get_ssh_client(reconnect=True)
372+
ssh_client_vm.execute("echo restart-test-2 > /root/restart_marker_2.txt; sync")
373+
374+
# 3) INCREMENTAL after the restart — the previously-broken path.
375+
Backup.create(self.apiclient, self.vm.id, "restart_incr")
376+
time.sleep(2)
377+
378+
backups = Backup.list(self.apiclient, self.vm.id)
379+
self.assertEqual(len(backups), 2,
380+
"Expected FULL + INCREMENTAL after restart, got %d" % len(backups))
381+
backups.sort(key=lambda b: b.created)
382+
self.assertEqual(self._backup_type(backups[0]).upper(), 'FULL',
383+
"First backup should be FULL")
384+
self.assertEqual(self._backup_type(backups[1]).upper(), 'INCREMENTAL',
385+
"Backup taken after the VM restart must be INCREMENTAL, not silently a FULL")
386+
387+
# 4) Restore the tip (incremental) and verify BOTH markers survived the chain
388+
# across the restart — i.e. the post-restart incremental really captured data.
389+
new_vm_name = "vm-restart-restore-" + str(int(time.time()))
390+
new_vm = Backup.createVMFromBackup(
391+
self.apiclient,
392+
self.services["small"],
393+
mode=self.services["mode"],
394+
backupid=backups[1].id,
395+
vmname=new_vm_name,
396+
accountname=self.account.name,
397+
domainid=self.account.domainid,
398+
zoneid=self.zone.id
399+
)
400+
self.cleanup.append(new_vm)
401+
self.assertIsNotNone(new_vm, "Failed to create VM from the post-restart incremental backup")
402+
self.assertEqual(new_vm.state, "Running", "Restored VM should be Running")
403+
404+
ssh_new = new_vm.get_ssh_client(reconnect=True)
405+
r1 = "".join(ssh_new.execute("cat /root/restart_marker_1.txt"))
406+
r2 = "".join(ssh_new.execute("cat /root/restart_marker_2.txt"))
407+
self.assertIn("restart-test-1", r1,
408+
"Marker written before the restart is missing from the restore")
409+
self.assertIn("restart-test-2", r2,
410+
"Marker written after the restart (captured by the post-restart incremental) "
411+
"is missing from the restore")
412+
finally:
413+
for b in reversed(backups):
414+
try:
415+
Backup.delete(self.apiclient, b.id)
416+
except Exception:
417+
pass
418+
self._set_full_every(original_full_every)
419+
self.backup_offering.removeOffering(self.apiclient, self.vm.id)
420+
337421
@attr(tags=["advanced", "backup"], required_hardware="true")
338422
def test_restore_from_incremental(self):
339423
"""

0 commit comments

Comments
 (0)