@@ -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