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

fix(migrate): a copied disk's source file is deleted only when no compute may use it

The source host alone was asked: a file on storage the computes share,
used by a VM or volume of another compute, was deleted under it. The
check the disk move makes, every compute and a host that cannot tell
counting as using it, moves to instances.utils and serves both. The
destination is not asked when its copy has the source's path: its VM
uses that copy, another file.

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 22:40:54 +03:00
parent c2c255a866
commit 826eaac813
6 changed files with 66 additions and 54 deletions

View file

@ -58,7 +58,7 @@ class MoveDiskJobTestCase(TransactionTestCase):
self.proxy.move_disk.assert_not_called() # nothing ran in the request
ctx = self.start(op)
get_kind(op.kind).precheck(op, ctx)
with patch("instances.jobs.wvmConnect") as hosts:
with patch("instances.utils.wvmConnect") as hosts:
hosts.return_value.paths_in_use.return_value = set()
result = get_kind(op.kind).run(op, ctx)
self.proxy.move_disk.assert_called_once()
@ -79,7 +79,7 @@ class MoveDiskJobTestCase(TransactionTestCase):
volume = self.proxy.disk_move_plan.return_value["volume"]
volume.delete.reset_mock()
volume.delete.side_effect = libvirtError("busy") if problem == "delete fails" else None
with patch("instances.jobs.wvmConnect") as hosts:
with patch("instances.utils.wvmConnect") as hosts:
reads = [here, other] if not isinstance(problem, libvirtError) else [here, problem]
hosts.return_value.paths_in_use.side_effect = reads
result = get_kind(op.kind).run(op, ctx)
@ -135,13 +135,13 @@ class MoveDiskJobTestCase(TransactionTestCase):
class UsedAnywhereTestCase(TransactionTestCase):
def test_a_host_that_does_not_answer_counts_as_using_the_file(self):
from instances import jobs
from instances import utils
Compute.objects.create(name="stuck", hostname="192.0.2.62", login="root", password="", type=1)
stuck = threading.Event()
self.addCleanup(stuck.set)
with patch("instances.jobs.wvmConnect") as hosts, patch.object(jobs, "USED_CHECK_WAIT", 0.2):
with patch("instances.utils.wvmConnect") as hosts, patch.object(utils, "USED_CHECK_WAIT", 0.2):
hosts.return_value.paths_in_use.side_effect = lambda: stuck.wait(10) and set()
started = time.monotonic()
self.assertTrue(jobs._used_anywhere(OLD))
self.assertTrue(utils.used_anywhere(OLD))
self.assertLess(time.monotonic() - started, 2)

View file

@ -34,18 +34,21 @@ class MigrationLogTests(TestCase):
self.proxy.remove_nvram.side_effect = remove_nvram
else:
self.proxy.remove_nvram.return_value = remove_nvram
self.proxy.paths_in_use.return_value = set(in_use)
if delete_fails:
self.proxy.get_volume_by_path.return_value.delete.side_effect = delete_fails
with patch.object(Compute, "status", True), \
patch("instances.utils.wvmInstances") as instances, \
patch("instances.utils.wvmInstance"), \
patch("instances.utils.wvmConnect") as hosts, \
patch.object(Instance, "proxy", self.proxy), \
patch.object(Instance, "autostart", False):
hosts.return_value.paths_in_use.return_value = set(in_use)
if isinstance(outcome, Exception):
instances.return_value.moveto.side_effect = outcome
else:
instances.return_value.moveto.return_value = (outcome, list(copied))
# each copy at the same path on the destination, unless a pair names its own
pairs = [item if isinstance(item, tuple) else (item, item) for item in copied]
instances.return_value.moveto.return_value = (outcome, pairs)
return migrate_instance(self.dest, self.vm, self.admin, **request)
def test_the_nvram_file_left_on_the_source_is_removed(self):
@ -128,6 +131,17 @@ class MigrationLogTests(TestCase):
self.vm.refresh_from_db()
self.assertEqual(self.vm.compute, self.dest)
def test_the_destination_is_asked_only_when_its_copy_has_another_path(self):
"""Its VM uses the copy: at the same path there, that is another file."""
path = "/var/lib/libvirt/images/vm.qcow2"
with patch("instances.utils.used_anywhere", return_value=False) as used:
self.migrate("live, disks copied: vda", copied=[path], live=True, delete_source=True)
self.assertEqual(used.call_args.kwargs, {"skip": self.dest.pk})
self.vm.compute = self.source
self.vm.save()
self.migrate("live, disks copied: vda", copied=[(path, "/srv/fast/vm.qcow2")], live=True, delete_source=True)
self.assertEqual(used.call_args.kwargs, {"skip": None})
def test_a_source_copy_another_vm_or_volume_uses_stays(self):
disks = ["/var/lib/libvirt/images/vm.qcow2", "/var/lib/libvirt/images/base.qcow2"]
self.migrate("live, disks copied: vda, vdb", copied=disks, in_use={disks[1]}, live=True,
@ -135,5 +149,5 @@ class MigrationLogTests(TestCase):
self.proxy.get_volume_by_path.assert_called_once_with(disks[0])
self.assertEqual(rows()[-2:], [
("VM vm: delete the source copy /var/lib/libvirt/images/vm.qcow2", True),
("VM vm: keep the source copy /var/lib/libvirt/images/base.qcow2: another VM or volume uses it", True),
("VM vm: keep the source copy /var/lib/libvirt/images/base.qcow2: a VM or volume of a compute may use it", True),
])