mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
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
153 lines
8.5 KiB
Python
153 lines
8.5 KiB
Python
"""A migration is logged where it runs (migrate_instance, in the job): the
|
|
mode it used on success, the error on failure, and each later step that
|
|
failed in a row of its own."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from computes.models import Compute
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import TestCase
|
|
from libvirt import libvirtError
|
|
from logs.models import Logs
|
|
|
|
from instances.models import Instance
|
|
from instances.utils import migrate_instance
|
|
from vrtManager import util
|
|
|
|
|
|
def rows():
|
|
return list(Logs.objects.order_by("id").values_list("message", "success"))
|
|
|
|
|
|
class MigrationLogTests(TestCase):
|
|
def setUp(self):
|
|
self.admin = get_user_model().objects.create_superuser("ml-admin", "ml@example.com", "pw")
|
|
self.source = Compute.objects.create(name="ml-src", hostname="198.51.100.8", login="root", password="", type=2)
|
|
self.dest = Compute.objects.create(name="ml-dst", hostname="198.51.100.9", login="root", password="", type=2)
|
|
self.vm = Instance.objects.create(compute=self.source, name="vm", uuid="u-ml")
|
|
|
|
def migrate(self, outcome, nvram=None, remove_nvram=True, copied=(), delete_fails=None, in_use=(), **request):
|
|
self.proxy = MagicMock()
|
|
self.proxy.get_autostart.return_value = False
|
|
self.proxy.get_nvram.return_value = nvram
|
|
if isinstance(remove_nvram, Exception):
|
|
self.proxy.remove_nvram.side_effect = remove_nvram
|
|
else:
|
|
self.proxy.remove_nvram.return_value = remove_nvram
|
|
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:
|
|
# 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):
|
|
self.migrate("live", nvram="/var/lib/libvirt/qemu/nvram/vm_VARS.fd", live=True)
|
|
path, destination = self.proxy.remove_nvram.call_args[0]
|
|
self.assertEqual(path, "/var/lib/libvirt/qemu/nvram/vm_VARS.fd")
|
|
self.assertIsNotNone(destination)
|
|
self.assertEqual(rows(), [("VM vm: migrate (live) to 198.51.100.9", True)])
|
|
|
|
def test_a_vm_without_nvram_removes_nothing(self):
|
|
self.migrate("live", live=True)
|
|
self.assertFalse(self.proxy.remove_nvram.called)
|
|
|
|
def test_a_failed_nvram_removal_is_logged_but_the_migration_stands(self):
|
|
self.migrate("live", nvram="/var/lib/libvirt/qemu/nvram/vm_VARS.fd",
|
|
remove_nvram=libvirtError("cannot delete"), live=True)
|
|
self.assertEqual(rows(), [
|
|
("VM vm: migrate (live) to 198.51.100.9", True),
|
|
("VM vm: remove NVRAM file /var/lib/libvirt/qemu/nvram/vm_VARS.fd on the source: cannot delete", False),
|
|
])
|
|
self.vm.refresh_from_db()
|
|
self.assertEqual(self.vm.compute, self.dest)
|
|
|
|
def test_any_nvram_removal_error_still_records_the_migration(self):
|
|
self.migrate("live", nvram="/var/lib/libvirt/qemu/nvram/vm_VARS.fd",
|
|
remove_nvram=AttributeError("unexpected"), live=True)
|
|
self.vm.refresh_from_db()
|
|
self.assertEqual(self.vm.compute, self.dest)
|
|
# an unknown error gives only its class name
|
|
self.assertEqual(rows()[-1], ("VM vm: remove NVRAM file /var/lib/libvirt/qemu/nvram/vm_VARS.fd on the source: "
|
|
"AttributeError", False))
|
|
|
|
def test_a_failed_migration_removes_nothing(self):
|
|
with self.assertRaises(util.OperationError):
|
|
self.migrate(util.OperationError("no"), nvram="/var/lib/libvirt/qemu/nvram/vm_VARS.fd", live=True)
|
|
self.assertFalse(self.proxy.remove_nvram.called)
|
|
|
|
def test_success_names_the_mode_used(self):
|
|
self.migrate("non-live, compressed", live=False, compress=True)
|
|
log = Logs.objects.get()
|
|
self.assertEqual(log.message, "VM vm: migrate (non-live, compressed) to 198.51.100.9")
|
|
self.assertEqual((log.user, log.host, log.instance, log.instance_uuid), ("ml-admin", "ml-src", "vm", "u-ml"))
|
|
|
|
def test_success_names_the_migration_address(self):
|
|
self.dest.migration_address = "203.0.113.9"
|
|
self.migrate("live", live=True)
|
|
self.assertEqual(Logs.objects.get().message, "VM vm: migrate (live) to 198.51.100.9 via 203.0.113.9")
|
|
self.vm.compute = self.source
|
|
self.vm.save()
|
|
self.dest.migration_address = "198.51.100.9" # the host name itself
|
|
self.migrate("live", live=True)
|
|
self.assertEqual(Logs.objects.latest("id").message, "VM vm: migrate (live) to 198.51.100.9")
|
|
|
|
def test_failure_is_logged_and_raised(self):
|
|
with self.assertRaisesRegex(util.OperationError, "A shut-off VM cannot be migrated live"):
|
|
self.migrate(util.OperationError("A shut-off VM cannot be migrated live"), live=True)
|
|
self.assertEqual(rows(), [("VM vm: migrate to 198.51.100.9: A shut-off VM cannot be migrated live", False)])
|
|
|
|
def test_the_source_copies_stay_unless_deleted(self):
|
|
disks = ["/var/lib/libvirt/images/vm.qcow2", "/var/lib/libvirt/images/vm-data.qcow2"]
|
|
self.migrate("live, disks copied: vda to default, vdb to default", copied=disks, live=True)
|
|
self.assertFalse(self.proxy.get_volume_by_path.called)
|
|
self.assertEqual(rows(), [
|
|
("VM vm: migrate (live, disks copied: vda to default, vdb to default) to 198.51.100.9", True),
|
|
("VM vm: keep the source copy /var/lib/libvirt/images/vm.qcow2", True),
|
|
("VM vm: keep the source copy /var/lib/libvirt/images/vm-data.qcow2", True),
|
|
])
|
|
|
|
def test_deleted_source_copies_and_a_failed_delete_are_logged(self):
|
|
self.migrate("live, disks copied: vda", copied=["/var/lib/libvirt/images/vm.qcow2"], live=True,
|
|
delete_source=True)
|
|
self.proxy.get_volume_by_path.assert_called_once_with("/var/lib/libvirt/images/vm.qcow2")
|
|
self.assertEqual(rows()[-1], ("VM vm: delete the source copy /var/lib/libvirt/images/vm.qcow2", True))
|
|
self.vm.compute = self.source
|
|
self.vm.save()
|
|
self.migrate("live, disks copied: vda", copied=["/var/lib/libvirt/images/vm.qcow2"], live=True,
|
|
delete_source=True, delete_fails=libvirtError("cannot delete"))
|
|
self.assertEqual(rows()[-1], ("VM vm: delete the source copy /var/lib/libvirt/images/vm.qcow2: cannot delete",
|
|
False))
|
|
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,
|
|
delete_source=True)
|
|
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: a VM or volume of a compute may use it", True),
|
|
])
|