diff --git a/README.md b/README.md index 9dcae29f..a69ce36f 100644 --- a/README.md +++ b/README.md @@ -533,6 +533,7 @@ A rollback leaves the clone on a detached HEAD at the restored commit; the updat - A template VM is changed or deleted only by superusers and staff owners with the matching permission; other owners can still view, open the console and clone it. Power cycle no longer starts a template. - A VM that vanishes from its compute keeps its owners for `INSTANCE_OWNERSHIP_RETENTION_DAYS` (30) in case the same UUID comes back; superusers list and remove these records on the "Removed VMs" admin page. - The log rotates (10 MB × 5) in `data/webvirtcloud.log`. +- A disk can be grown while its VM runs or is paused, not only when it is shut off. - gstfsd finds the guest's operating system the way libguestfs inspects it, so a root on LVM works, and it always answers (an error when it finds no or several operating systems). Update `/usr/local/bin/gstfsd` on every compute (see [Adding a Compute Node](#adding-a-compute-node), step 3). - Docker no longer publishes port 6080; the console goes through nginx on the panel's port (`WS_PUBLIC_PORT`, default 80). diff --git a/instances/templates/instances/resize_tab.html b/instances/templates/instances/resize_tab.html index 7d389bde..06006be3 100644 --- a/instances/templates/instances/resize_tab.html +++ b/instances/templates/instances/resize_tab.html @@ -148,7 +148,7 @@ {% endif %} {% endfor %} - {% if instance.status == 5 %} + {% if instance.status == 1 or instance.status == 3 or instance.status == 5 %} {% else %} diff --git a/instances/test_live_disk_resize.py b/instances/test_live_disk_resize.py new file mode 100644 index 00000000..3da911e6 --- /dev/null +++ b/instances/test_live_disk_resize.py @@ -0,0 +1,86 @@ +"""Live checks that a disk grows while its VM runs or is paused. + +QEMU holds a running domain's image, so the size goes through blockResize; +the guest and the volume must both see it, and the persistent definition +stays as it was. Runs only with TEST_LIBVIRT_HOST set (see instances/livetest.py). +""" + +import os +import unittest + +import libvirt +from django.test import SimpleTestCase +from vrtManager.connection import connection_manager +from vrtManager.instance import wvmInstance + +from . import livetest + +P = livetest.PREFIX +PERSISTENT = libvirt.VIR_DOMAIN_XML_INACTIVE | libvirt.VIR_DOMAIN_XML_SECURE + + +class LiveDiskResizeTestCase(SimpleTestCase): + @classmethod + def setUpClass(cls): + if not livetest.enabled(): + raise unittest.SkipTest("Set TEST_LIBVIRT_HOST to run the live libvirt tests") + cls.args = ( + os.environ["TEST_LIBVIRT_HOST"], + os.environ.get("TEST_LIBVIRT_LOGIN", ""), + os.environ.get("TEST_LIBVIRT_PASSWORD", ""), + int(os.environ.get("TEST_LIBVIRT_TYPE", 4)), + ) + cls.conn = connection_manager.get_connection(*cls.args) + livetest.cleanup(cls.conn) + livetest.ensure_pool(cls.conn) + cls.inventory = livetest.inventory(cls.conn) + super().setUpClass() + + @classmethod + def tearDownClass(cls): + try: + livetest.remove_pool(cls.conn) + finally: + super().tearDownClass() + if livetest.inventory(cls.conn) != cls.inventory: + raise AssertionError("Live tests changed objects outside the test namespace") + + def tearDown(self): + livetest.cleanup(self.conn) + + def vm(self, name, fmt): + pool = self.conn.storagePoolLookupByName(livetest.POOL) + vol = pool.createXML( + f"{P}{name}.{fmt}64" + f"", + 0, + ) + dom = self.conn.defineXML( + f"{P}{name}1281" + "hvm" + f"" + f"" + ) + return dom, vol, wvmInstance(*self.args, f"{P}{name}") + + def test_a_disk_grows_while_the_vm_runs_or_is_paused(self): + new_size = 96 << 20 + for state in ("running", "paused"): + for fmt in ("qcow2", "raw"): + with self.subTest(state=state, fmt=fmt): + try: + dom, vol, vm = self.vm(f"resize-{state}-{fmt}", fmt) + dom.create() + if state == "paused": + dom.suspend() + before = dom.XMLDesc(PERSISTENT) + + disk = vm.get_disk_devices()[0] + vm.resize_disk([{"path": disk["path"], "size_new": new_size}]) + + self.assertEqual(dom.blockInfo(disk["path"])[0], new_size, "the guest's disk") + self.assertEqual(vol.info()[1], new_size, "the volume") + self.assertEqual(dom.XMLDesc(PERSISTENT), before) + self.assertEqual(dom.state()[0], libvirt.VIR_DOMAIN_PAUSED if state == "paused" else libvirt.VIR_DOMAIN_RUNNING) + finally: + livetest.cleanup(self.conn) diff --git a/instances/test_page_controls.py b/instances/test_page_controls.py index 699043e2..8ff71945 100644 --- a/instances/test_page_controls.py +++ b/instances/test_page_controls.py @@ -150,6 +150,18 @@ class PageControlsTestCase(PageControls, TestCase): owner = self.user("paused_deleter", owner={"is_delete": True}) self.assertIn("destroy", self.controls(owner, status=3)) + def test_disks_grow_while_running_paused_or_shut_off(self): + # QEMU grows a running or paused VM's disk itself (blockResize) + changer = self.user("disk_resizer", owner={"is_change": True}) + for status, enabled in ((1, True), (3, True), (5, True), (7, False)): + with self.subTest(status=status): + self.controls(changer, status=status) + buttons = lxml_html.fromstring(self.last_page).xpath( + "//form[@action=$a]//button[@type='submit']", + a=reverse("instances:resize_disk", args=[self.instance.id]), + ) + self.assertEqual(bool(buttons), enabled) + def test_owner_with_change_and_no_extra_permissions(self): changer = self.user("ctl_plain_changer", owner={"is_change": True}) self.assertEqual(self.controls(changer), CONSOLE | POWER | CHANGE)