mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
feat(instances): grow a disk while the VM runs or is paused
The resize tab enabled disk growth only for a shut-off VM, although resize_disk already grows a running domain's disk through blockResize. The button is now enabled for running and paused VMs too. A live test grows qcow2 and raw disks of a running and of a paused VM and checks the size the guest sees, the volume size, the unchanged persistent definition and the state.
This commit is contained in:
parent
342f6a7135
commit
e14a4cc10d
4 changed files with 100 additions and 1 deletions
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if instance.status == 5 %}
|
||||
{% if instance.status == 1 or instance.status == 3 or instance.status == 5 %}
|
||||
<button type="submit" class="btn btn-lg btn-success float-end" name="resizevm_disk">{% trans "Resize" %}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-lg btn-success float-end disabled">{% trans "Resize" %}</button>
|
||||
|
|
|
|||
86
instances/test_live_disk_resize.py
Normal file
86
instances/test_live_disk_resize.py
Normal file
|
|
@ -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"<volume><name>{P}{name}.{fmt}</name><capacity unit='MiB'>64</capacity>"
|
||||
f"<target><format type='{fmt}'/></target></volume>",
|
||||
0,
|
||||
)
|
||||
dom = self.conn.defineXML(
|
||||
f"<domain type='kvm'><name>{P}{name}</name><memory unit='MiB'>128</memory><vcpu>1</vcpu>"
|
||||
"<os><type arch='x86_64' machine='q35'>hvm</type></os><features><acpi/></features><devices>"
|
||||
f"<disk type='file' device='disk'><driver name='qemu' type='{fmt}'/>"
|
||||
f"<source file='{vol.path()}'/><target dev='vda' bus='virtio'/></disk></devices></domain>"
|
||||
)
|
||||
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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue