mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 03:36:48 +00:00
fix(storages): a volume a running VM uses is not cloned
The copy was read while the guest wrote: a raw disk was copied with data of different instants, and a qcow2 one failed on qemu-img's image lock with a message that did not say why. Such a clone is refused, with the running VMs named, when it is queued and again before it runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kk4h2TcUiaWmU5MUszYSea
This commit is contained in:
parent
50ed08f3ca
commit
018ca69a3b
8 changed files with 266 additions and 213 deletions
|
|
@ -351,6 +351,16 @@ class wvmStorage(wvmConnect):
|
|||
return f"{target_file}.{vol_fmt}"
|
||||
return f"{target_file}.{file_suffix}" if file_suffix else target_file
|
||||
|
||||
def refuse_running_copy(self, name):
|
||||
"""A volume a running guest writes is not copied: the copy would mix
|
||||
instants."""
|
||||
path, pool = self.get_volume(name).path(), self.pool.name()
|
||||
# a disk names it by its path (an RBD image by pool/image) or by its pool and name
|
||||
uses = "devices/disk/source[@file=$p or @dev=$p or @name=$p or (@pool=$pool and @volume=$v)]"
|
||||
if vms := self.domains_using(lambda domain: domain.xpath(uses, p=path, pool=pool, v=name), running_only=True):
|
||||
raise util.OperationError(gettext_noop("The running VMs %(vms)s use volume %(name)s; shut them down to copy it"),
|
||||
vms=", ".join(vms), name=name)
|
||||
|
||||
def clone_volume(
|
||||
self,
|
||||
name,
|
||||
|
|
|
|||
|
|
@ -132,3 +132,24 @@ class DeleteVolumeTestCase(unittest.TestCase):
|
|||
conn, vols = storage([domain("u1", ["/pool/other.qcow2"])], [volume("/pool/disk.qcow2")])
|
||||
conn.del_volume("disk.qcow2")
|
||||
vols["disk.qcow2"].delete.assert_called_once_with(0)
|
||||
|
||||
|
||||
class RunningCopyTestCase(unittest.TestCase):
|
||||
"""A running guest's disk is not copied: the copy would mix instants."""
|
||||
|
||||
def test_each_way_a_disk_names_the_volume_refuses_its_copy(self):
|
||||
for source in ("file='/pool/disk.qcow2'", "dev='/pool/disk.qcow2'", "protocol='rbd' name='/pool/disk.qcow2'",
|
||||
"pool='pool' volume='disk.qcow2'"):
|
||||
with self.subTest(source=source):
|
||||
dom = MagicMock(**{"name.return_value": "vm"})
|
||||
dom.XMLDesc.return_value = f"<domain><devices><disk device='disk'><source {source}/></disk></devices></domain>"
|
||||
conn, _ = storage([dom], [volume("/pool/disk.qcow2")])
|
||||
conn.pool.name.return_value = "pool"
|
||||
with self.assertRaisesRegex(util.OperationError, "The running VMs vm use volume disk.qcow2"):
|
||||
conn.refuse_running_copy("disk.qcow2")
|
||||
conn.wvm.listAllDomains.assert_called_once_with(libvirt.VIR_CONNECT_LIST_DOMAINS_ACTIVE)
|
||||
|
||||
def test_another_disk_does_not(self):
|
||||
conn, _ = storage([domain("u1", ["/pool/other.qcow2"])], [volume("/pool/disk.qcow2")])
|
||||
conn.pool.name.return_value = "pool"
|
||||
conn.refuse_running_copy("disk.qcow2")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue