mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
Settings > Migrate was a horizontal form of six switches without explanations. The owner chose this layout from three rendered options: the route on top as "From" (the source host) and "To" (the destination select) side by side, a "Storage" card with where each disk goes, a "Transfer settings" card with the switches in two columns, each with a line saying what it does, and a footer. Codex reviewed migration end to end; these are fixed: - No Offline switch: the mode follows the VM's state, and one line says which. A shut-off VM moved offline whether the switch was on or off. - Live migration shows for a running or paused VM; Auto converge, Compressed and Unsafe only for a running VM, the only state the migration applies them to. Unsafe was offered for a paused or shut-off VM and ignored. - Unsafe says it skips libvirt's safety checks; Delete source files says it deletes only the files of copied disks. - A VM that cannot migrate anywhere (no other host, a state that does not migrate, snapshots, host devices) gets the reason and a disabled button instead of the disk plan and the switches; it learned that after submitting. - Migrate stays disabled while the destination did not answer or a disk cannot go there; it was enabled after any plan.
84 lines
4.5 KiB
Python
84 lines
4.5 KiB
Python
"""The migrate form shows the switches the VM's state uses and ticks auto
|
|
converge for a running VM, and the API uses the same default: it only slows
|
|
a VM that the migration cannot keep up with. A VM that cannot migrate gets
|
|
the reason instead."""
|
|
|
|
from unittest.mock import patch
|
|
|
|
import libvirt
|
|
from computes.models import Compute
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
from instances.models import Instance
|
|
from lxml import html
|
|
from operations.models import Operation
|
|
from rest_framework.test import APIClient
|
|
|
|
DOMAIN = """<domain type='test'><name>migrate-form-vm</name><memory unit='MiB'>128</memory><vcpu>1</vcpu>
|
|
<os><type arch='i686'>hvm</type></os></domain>"""
|
|
|
|
|
|
class MigrateFormTests(TestCase):
|
|
def setUp(self):
|
|
self.conn = libvirt.open("test:///default")
|
|
self.addCleanup(self.conn.close)
|
|
self.domain = self.conn.defineXML(DOMAIN)
|
|
self.addCleanup(self.remove_domain)
|
|
self.compute = Compute.objects.create(name="mf", hostname="localhost", login="", password="", type=4)
|
|
self.other = Compute.objects.create(name="mf-other", hostname="198.51.100.30", login="", password="", type=4)
|
|
self.vm = Instance.objects.create(compute=self.compute, name=self.domain.name(), uuid=self.domain.UUIDString())
|
|
for target, value in (("get_machine_types", ["pc"]), ("get_nwfilters", [])):
|
|
patcher = patch(f"vrtManager.connection.wvmConnect.{target}", return_value=value)
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
self.admin = get_user_model().objects.create_superuser("mf-admin", "mf@example.com", "pw")
|
|
|
|
def remove_domain(self):
|
|
if self.domain.isActive():
|
|
self.domain.destroy()
|
|
self.domain.undefine()
|
|
|
|
def migrate_form(self):
|
|
self.client.force_login(self.admin)
|
|
with patch("vrtManager.connection.connection_manager.get_connection", return_value=self.conn):
|
|
page = self.client.get(reverse("instances:instance", args=[self.vm.id])).content
|
|
(form,) = html.fromstring(page).xpath("//form[@action=$a]", a=reverse("instances:migrate", args=[self.vm.id]))
|
|
return form
|
|
|
|
def test_auto_converge_is_ticked_for_a_running_vm(self):
|
|
self.domain.create()
|
|
self.assertEqual(self.migrate_form().xpath(".//input[@name='autoconverge']/@checked"), ["checked"])
|
|
|
|
def test_each_state_shows_the_switches_it_uses(self):
|
|
# the mode follows the state: no Offline switch; unsafe, auto converge and compressed apply to a running VM
|
|
for state, switches in (("shut off", ["delete_source"]),
|
|
("running", ["live_migrate", "autoconverge", "compress", "unsafe_migrate", "delete_source"]),
|
|
("paused", ["live_migrate", "delete_source"])):
|
|
with self.subTest(state):
|
|
if state == "running":
|
|
self.domain.create()
|
|
elif state == "paused":
|
|
self.domain.suspend()
|
|
self.assertEqual(self.migrate_form().xpath(".//input[@type='checkbox']/@name"), switches)
|
|
|
|
def test_a_vm_with_no_other_host_gets_the_reason(self):
|
|
self.other.delete()
|
|
form = self.migrate_form()
|
|
self.assertIn("There is no other host to migrate to.", form.text_content())
|
|
self.assertEqual(form.xpath(".//button[@name='migrate']"), [])
|
|
|
|
def test_the_api_uses_auto_converge_unless_told_otherwise(self):
|
|
api = APIClient()
|
|
api.force_authenticate(self.admin)
|
|
request = {"instance": self.vm.id, "target_compute": self.other.id, "live": True, "offline": False}
|
|
with patch("instances.api.viewsets.jobs.submit") as submit:
|
|
submit.return_value = Operation.objects.create(kind="instance.migrate", user=self.admin,
|
|
username=self.admin.username, submission_key="key-api00000")
|
|
for n, encoding in enumerate(("json", "multipart"), 1):
|
|
api.post("/api/v1/migrate/", {**request, "submission_key": f"key-api0000{n}"}, format=encoding)
|
|
api.post("/api/v1/migrate/", {**request, "autoconverge": False, "submission_key": "key-api00003"},
|
|
format="json")
|
|
api.post("/api/v1/migrate/", {**request, "unsafe": True, "submission_key": "key-api00004"}, format="json")
|
|
self.assertEqual([c.args[2]["autoconverge"] for c in submit.call_args_list], [True, True, False, True])
|
|
self.assertEqual([c.args[2]["unsafe"] for c in submit.call_args_list], [False, False, False, True])
|