1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 12:46:49 +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:
catborise 2026-11-07 23:40:28 +03:00
parent 50ed08f3ca
commit 018ca69a3b
8 changed files with 266 additions and 213 deletions

View file

@ -55,8 +55,10 @@ class CloneVolume(Kind):
if op.user is None or not op.user.is_active or not op.user.is_superuser:
raise PermissionDenied
# a volume made outside the panel since the job was queued may have taken the name
if op.intent["target"] in _storage(op.params)[1].get_volumes():
stg = _storage(op.params)[1]
if op.intent["target"] in stg.get_volumes():
raise util.OperationError(gettext_noop("Name of volume already in use"))
stg.refuse_running_copy(op.params["image"]) # the VM may have started since
def plan(self, user, params):
compute, stg = _storage(params)
@ -65,6 +67,7 @@ class CloneVolume(Kind):
raise util.OperationError(gettext_noop("The pool has no volume named %(name)s"), name=params["image"])
if is_iso(params["image"]):
raise util.OperationError(gettext_noop("An ISO image is not cloned"))
stg.refuse_running_copy(params["image"])
target = stg.clone_name(params["name"], params["format"] or stg.get_volume_format_type(params["image"]))
if target in volumes:
raise util.OperationError(gettext_noop("Name of volume already in use"))

View file

@ -13,6 +13,7 @@ from operations.jobs import Context
from operations.models import Claim, Operation, acquire
from libvirt import libvirtError
from storages.jobs import CloneVolume
from vrtManager import util
class VolumeCloneJobTestCase(TransactionTestCase):
@ -64,6 +65,12 @@ class VolumeCloneJobTestCase(TransactionTestCase):
response = self.client.post(url, {"cln_volume": "1", "image": image, "name": "copy",
"submission_key": key}, follow=True)
self.assertContains(response, why)
# a running guest's disk would be copied while it writes
self.stg.get_volumes.return_value = ["base.qcow2"]
self.stg.refuse_running_copy.side_effect = util.OperationError("The running VMs vm use volume base.qcow2")
response = self.client.post(url, {"cln_volume": "1", "image": "base.qcow2", "name": "copy",
"submission_key": "key-vol00004"}, follow=True)
self.assertContains(response, "The running VMs vm use volume base.qcow2")
self.assertFalse(Operation.objects.exists())
def test_recovery(self):