mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
A migration needed every disk on storage both hosts share: it was refused
when the destination lacked a disk file, and a file of the same name
there was taken for the VM's own disk.
The Migrate tab now lists the VM's disk files for the chosen destination.
For each disk it offers the destination's pools that keep disk files
(directory, filesystem and NFS pools). A disk on storage both hosts share
can also stay where it is.
A directory counts as shared when both hosts have it as a storage pool
and a marker file created on the source exists on the destination. A
directory the probe cannot tell is never taken as shared.
libvirt copies the chosen disks during the migration: migrate3 with
NON_SHARED_DISK for those targets, and destination and persistent
definitions naming the new paths, where libvirt creates the files.
- A shut-off VM is started paused for the copy and shut off on the
destination; its CPUs never run. Its UEFI variables are copied first,
as the destination would start from the firmware's template. If the
migration fails, the VM is shut off on the source again.
- Refused before anything changes, each disk with its reason:
- a disk neither shared nor given a pool, also one only the next
start's definition has;
- CD-ROM images, read-only or encrypted disks, formats other than raw
and qcow2, and disks with a backing file;
- a pool that is not of disk files, or that has a file of that name,
which libvirt would reuse and overwrite;
- two disks copied to one file.
- The job claims each copy's name in its pool, and the source volume.
- A failed or cancelled migration deletes the copies libvirt made, and
the UEFI variables copy, unless a VM on the destination has them.
- The source files stay and are named in the log. With "Delete the
source files of copied disks" they go, except one another VM or volume
uses; a failed delete is logged and the migration stands.
The NVRAM sharing check uses the same probe: a shut-off move stops when
it cannot be told, and the source file stays.
The API's migrate call takes disk_pools ({target: pool}) and
delete_source.
139 lines
7.5 KiB
Python
139 lines
7.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
|
|
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.object(Instance, "proxy", self.proxy), \
|
|
patch.object(Instance, "autostart", False):
|
|
if isinstance(outcome, Exception):
|
|
instances.return_value.moveto.side_effect = outcome
|
|
else:
|
|
instances.return_value.moveto.return_value = (outcome, list(copied))
|
|
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_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: another VM or volume uses it", True),
|
|
])
|