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:
parent
c2c255a866
commit
826eaac813
6 changed files with 66 additions and 54 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue