1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 12:46:49 +00:00

fix(snapshot): preserve loader XML during snapshot operations

This commit is contained in:
catborise 2026-09-28 14:55:24 +03:00
parent ce67778d2f
commit 30c436ecde
2 changed files with 125 additions and 17 deletions

View file

@ -1,8 +1,11 @@
import contextlib import contextlib
import json import json
import logging
import os.path import os.path
import time import time
logger = logging.getLogger(__name__)
try: try:
from libvirt import ( from libvirt import (
VIR_DOMAIN_AFFECT_CONFIG, VIR_DOMAIN_AFFECT_CONFIG,
@ -1277,28 +1280,43 @@ class wvmInstance(wvmConnect):
state, state,
time.time(), time.time(),
) )
self.change_snapshot_xml() changed = self.change_snapshot_xml()
try: try:
xml += self._XMLDesc(VIR_DOMAIN_XML_SECURE) xml += self._XMLDesc(VIR_DOMAIN_XML_SECURE)
xml += """<active>0</active> xml += """<active>0</active>
</domainsnapshot>""" </domainsnapshot>"""
self._snapshotCreateXML(xml, 0) self._snapshotCreateXML(xml, 0)
finally: finally:
self.recover_snapshot_xml() if changed:
self.recover_snapshot_xml()
def change_snapshot_xml(self): def change_snapshot_xml(self):
xml_temp = self._XMLDesc(VIR_DOMAIN_XML_SECURE).replace( try:
"<loader readonly='yes' type='pflash'>", raw_xml = self._XMLDesc(VIR_DOMAIN_XML_SECURE)
"<loader readonly='yes' type='rom'>", tree = ElementTree.fromstring(raw_xml)
) loader = tree.find("./os/loader")
self._defineXML(xml_temp) if loader is not None and loader.get("type") == "pflash":
loader.set("type", "rom")
self._defineXML(ElementTree.tostring(tree, encoding="unicode"))
return True
return False
except Exception as e:
logger.error("Failed to change snapshot XML loader: %s", e)
raise
def recover_snapshot_xml(self): def recover_snapshot_xml(self):
xml_temp = self._XMLDesc(VIR_DOMAIN_XML_SECURE).replace( try:
"<loader readonly='yes' type='rom'>", raw_xml = self._XMLDesc(VIR_DOMAIN_XML_SECURE)
"<loader readonly='yes' type='pflash'>", tree = ElementTree.fromstring(raw_xml)
) loader = tree.find("./os/loader")
self._defineXML(xml_temp) if loader is not None and loader.get("type") == "rom":
loader.set("type", "pflash")
self._defineXML(ElementTree.tostring(tree, encoding="unicode"))
return True
return False
except Exception as e:
logger.error("Failed to recover snapshot XML loader: %s", e)
raise
def create_external_snapshot(self, name, date=None, desc=None): def create_external_snapshot(self, name, date=None, desc=None):
creation_time = time.time() creation_time = time.time()
@ -1318,7 +1336,7 @@ class wvmInstance(wvmConnect):
creation_time, creation_time,
) )
self.change_snapshot_xml() changed = self.change_snapshot_xml()
try: try:
xml += self._XMLDesc(VIR_DOMAIN_XML_SECURE) xml += self._XMLDesc(VIR_DOMAIN_XML_SECURE)
xml += """<active>0</active> xml += """<active>0</active>
@ -1326,7 +1344,8 @@ class wvmInstance(wvmConnect):
self._snapshotCreateXML(xml, VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY) self._snapshotCreateXML(xml, VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY)
finally: finally:
self.recover_snapshot_xml() if changed:
self.recover_snapshot_xml()
self.refresh_instance_pools() self.refresh_instance_pools()
def get_external_snapshots(self): def get_external_snapshots(self):
@ -1402,12 +1421,13 @@ class wvmInstance(wvmConnect):
snap.delete(0) snap.delete(0)
def snapshot_revert(self, snapshot): def snapshot_revert(self, snapshot):
self.change_snapshot_xml() changed = self.change_snapshot_xml()
try: try:
snap = self.instance.snapshotLookupByName(snapshot, 0) snap = self.instance.snapshotLookupByName(snapshot, 0)
self.instance.revertToSnapshot(snap, 0) self.instance.revertToSnapshot(snap, 0)
finally: finally:
self.recover_snapshot_xml() if changed:
self.recover_snapshot_xml()
def get_managed_save_image(self): def get_managed_save_image(self):
return self.instance.hasManagedSaveImage(0) return self.instance.hasManagedSaveImage(0)

View file

@ -69,9 +69,28 @@ class TestXmlSafety(unittest.TestCase):
inst.create_snapshot(bad_name, bad_desc) inst.create_snapshot(bad_name, bad_desc)
inst.change_snapshot_xml.assert_called_once() inst.change_snapshot_xml.assert_called_once()
# Verify recover_snapshot_xml was called in finally despite error
inst.recover_snapshot_xml.assert_called_once() inst.recover_snapshot_xml.assert_called_once()
def test_snapshot_xml_payload_is_escaped(self):
inst = wvmInstance.__new__(wvmInstance)
inst.get_status = MagicMock(return_value=5) # shutoff
inst.change_snapshot_xml = MagicMock()
inst.recover_snapshot_xml = MagicMock()
inst._XMLDesc = MagicMock(return_value="<domain></domain>")
inst._snapshotCreateXML = MagicMock(return_value=None)
bad_name = "snap<inject>&quotes"
bad_desc = "desc<tag>&and"
inst.create_snapshot(bad_name, bad_desc)
inst._snapshotCreateXML.assert_called_once()
created_xml = inst._snapshotCreateXML.call_args[0][0]
self.assertNotIn("<inject>", created_xml)
self.assertNotIn("<tag>", created_xml)
self.assertIn("&lt;inject&gt;&amp;quotes", created_xml)
self.assertIn("&lt;tag&gt;&amp;and", created_xml)
def test_external_snapshot_recovery(self): def test_external_snapshot_recovery(self):
inst = wvmInstance.__new__(wvmInstance) inst = wvmInstance.__new__(wvmInstance)
inst.get_status = MagicMock(return_value=1) inst.get_status = MagicMock(return_value=1)
@ -87,6 +106,75 @@ class TestXmlSafety(unittest.TestCase):
inst.recover_snapshot_xml.assert_called_once() inst.recover_snapshot_xml.assert_called_once()
inst.refresh_instance_pools.assert_called_once() inst.refresh_instance_pools.assert_called_once()
def test_secure_boot_loader_change_and_recovery(self):
inst = wvmInstance.__new__(wvmInstance)
inst.wvm = MagicMock()
uefi_xml = "<domain><os><loader readonly='yes' secure='yes' type='pflash'>/usr/share/OVMF/OVMF.fd</loader></os></domain>"
inst._XMLDesc = MagicMock(return_value=uefi_xml)
inst.change_snapshot_xml()
inst.wvm.defineXML.assert_called_once()
changed_xml = inst.wvm.defineXML.call_args[0][0]
self.assertIn("type=\"rom\"", changed_xml)
self.assertIn("secure=\"yes\"", changed_xml)
# Test recovery from rom back to pflash
inst._XMLDesc = MagicMock(return_value=changed_xml)
inst.wvm.defineXML.reset_mock()
inst.recover_snapshot_xml()
inst.wvm.defineXML.assert_called_once()
recovered_xml = inst.wvm.defineXML.call_args[0][0]
self.assertIn("type=\"pflash\"", recovered_xml)
self.assertIn("secure=\"yes\"", recovered_xml)
def test_original_rom_loader_preserved(self):
inst = wvmInstance.__new__(wvmInstance)
inst._XMLDesc = MagicMock(return_value="<domain><os><loader type='rom'>/usr/share/rom.bin</loader></os></domain>")
inst._defineXML = MagicMock()
inst._snapshotCreateXML = MagicMock(return_value=None)
inst.get_status = MagicMock(return_value=5)
# change_snapshot_xml should return False and not define XML
self.assertFalse(inst.change_snapshot_xml())
inst._defineXML.assert_not_called()
# Create snapshot should not invoke recover_snapshot_xml if not changed
inst.change_snapshot_xml = MagicMock(return_value=False)
inst.recover_snapshot_xml = MagicMock()
inst.create_snapshot("snap_rom", "testing rom preservation")
inst.change_snapshot_xml.assert_called_once()
inst.recover_snapshot_xml.assert_not_called()
def test_malformed_xml_raises_in_change(self):
inst = wvmInstance.__new__(wvmInstance)
inst._XMLDesc = MagicMock(return_value="<malformed << xml")
with self.assertRaises(Exception):
inst.change_snapshot_xml()
def test_recovery_failure_raises(self):
inst = wvmInstance.__new__(wvmInstance)
inst._XMLDesc = MagicMock(return_value="<domain><os><loader type='rom'>/path</loader></os></domain>")
inst._defineXML = MagicMock(side_effect=RuntimeError("libvirt defineXML failed"))
with self.assertRaises(RuntimeError):
inst.recover_snapshot_xml()
def test_snapshot_revert_rom_loader_preserved(self):
inst = wvmInstance.__new__(wvmInstance)
inst.instance = MagicMock()
mock_snap = MagicMock()
inst.instance.snapshotLookupByName = MagicMock(return_value=mock_snap)
inst.instance.revertToSnapshot = MagicMock()
inst.change_snapshot_xml = MagicMock(return_value=False)
inst.recover_snapshot_xml = MagicMock()
inst.snapshot_revert("snap_rom")
inst.change_snapshot_xml.assert_called_once()
inst.instance.snapshotLookupByName.assert_called_once_with("snap_rom", 0)
inst.instance.revertToSnapshot.assert_called_once_with(mock_snap, 0)
inst.recover_snapshot_xml.assert_not_called()
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()