diff --git a/README.md b/README.md index 149f8eee..f9ace2e1 100644 --- a/README.md +++ b/README.md @@ -539,6 +539,7 @@ A rollback leaves the clone on a detached HEAD at the restored commit; the updat - An ISO uploads to an SSH compute about 3.5 times faster (SFTP writes no longer wait for each 32 KiB); a failed write still fails the upload. - The VM page shows each NIC's own QoS and each CD-ROM's own pool (they showed the previous device's), and a NIC without a source or model no longer breaks it. - A volume of an LVM or iSCSI pool can be attached to a VM, and bridge and direct NICs can be boot devices. The NIC model `default` lets libvirt choose, and `rt18139` is `rtl8139`: a VM with either did not start; a migration corrects the default NIC type setting. +- A paused VM's memory is resized as a running VM's: the current memory changes and the guest takes it once resumed, the maximum stays (both only reached the VM at its next boot). - Cloning a UEFI VM no longer defines a storage pool on the host's NVRAM directory; the clone gets its own copy of the VM's UEFI variables. - The console .vv file finds the VM by its UUID; without a listen address or password it gives 127.0.0.1 and an empty password instead of "None". - A running VM without a guest agent shows the IPv4 address the host learned by ARP (the lookup always failed before). diff --git a/instances/test_live_guest.py b/instances/test_live_guest.py index 566c9dad..1ffd3ec9 100644 --- a/instances/test_live_guest.py +++ b/instances/test_live_guest.py @@ -380,3 +380,22 @@ class LiveGuestTestCase(TestCase): self.assertEqual(dom.state()[0], libvirt.VIR_DOMAIN_RUNNING) self.wait_agent(dom) self.assert_guest_matches_config(dom) + + def test_a_paused_guest_gets_its_new_memory_once_resumed(self): + dom, inst = self.boot("gmem") + + def balloon_mib(): + return dom.memoryStats().get("actual", 0) >> 10 + + def config_mib(tag): + return int(etree.fromstring(dom.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)).findtext(tag)) >> 10 + + for cur in (1536, 2048): # down, then back up to the maximum + self.post("suspend", inst) + # the posted maximum is ignored: an active VM keeps its own + self.post("resize_memory", inst, {"memory": "4096", "cur_memory": str(cur)}) + self.assertEqual(dom.state()[0], libvirt.VIR_DOMAIN_PAUSED) + self.assertEqual((config_mib("currentMemory"), config_mib("memory")), (cur, 2048)) + self.post("resume", inst) + self.assertTrue(wait_for(lambda: balloon_mib() == cur, 60), f"balloon at {balloon_mib()} MiB, not {cur}") + self.wait_agent(dom) diff --git a/instances/test_missing_fields.py b/instances/test_missing_fields.py index 83b721fb..d94ad3fd 100644 --- a/instances/test_missing_fields.py +++ b/instances/test_missing_fields.py @@ -64,7 +64,7 @@ class MissingFieldTestCase(TestCase): self.assert_reported(self.post("resizevm_cpu", {"vcpu": "²", "cur_vcpu": "1"})) def test_resize_memory_with_a_unicode_digit(self): - self.assert_reported(self.post("resize_memory", {"memory_custom": "²", "cur_memory": "512"})) + self.assert_reported(self.post("resize_memory", {"memory": "512", "cur_memory_custom": "²"})) def test_set_qos_with_non_numeric_rate(self): self.assert_reported(self.post("set_qos", {"qos_direction": "inbound", "net-mac-0": "52:54:00:00:00:01", "qos_average": "fast"})) diff --git a/instances/test_quota.py b/instances/test_quota.py index d9ef4dba..4c9757e6 100644 --- a/instances/test_quota.py +++ b/instances/test_quota.py @@ -13,7 +13,7 @@ from django.test import TestCase from django.urls import reverse from instances.models import Instance from instances.utils import QUOTA_UNVERIFIED, check_user_quota -from libvirt import VIR_DOMAIN_RUNNING +from libvirt import VIR_DOMAIN_PAUSED, VIR_DOMAIN_RUNNING from vrtManager.util import OperationError @@ -131,6 +131,7 @@ class QuotaViewTests(TestCase): wvm.return_value.get_memory.return_value = 2048 wvm.return_value.get_cur_memory.return_value = 2048 wvm.return_value.get_status.return_value = status + wvm.return_value.instance.isActive.return_value = status != 5 response = self.client.post(reverse(view, args=[self.vm.id]), data) messages = [str(m) for m in get_messages(response.wsgi_request)] return messages, check, wvm.return_value @@ -150,6 +151,11 @@ class QuotaViewTests(TestCase): # resize_mem would write it, and it must not be the unchecked 8192 proxy.resize_mem.assert_called_once_with("1024", 2048) + def test_paused_vm_memory_charges_no_maximum_change(self): + _, check, proxy = self.post("instances:resize_memory", {"memory": 8192, "cur_memory": 1024}, status=VIR_DOMAIN_PAUSED) + self.assertEqual(check.call_args.args[1:], (0, 0, 0, 0)) + proxy.resize_mem.assert_called_once_with("1024", 2048) + def test_shut_off_vm_memory_charges_the_maximum_change(self): _, check, _ = self.post("instances:resize_memory", {"memory": 4096, "cur_memory": 1024}, status=5) self.assertEqual(check.call_args.args[1:], (0, 0, 2048, 0)) diff --git a/instances/views.py b/instances/views.py index fa85f56e..0581b6cb 100755 --- a/instances/views.py +++ b/instances/views.py @@ -36,7 +36,6 @@ from libvirt import (VIR_DOMAIN_UNDEFINE_KEEP_NVRAM, VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA, VIR_DOMAIN_UNDEFINE_NVRAM, VIR_DOMAIN_START_PAUSED, - VIR_DOMAIN_RUNNING, libvirtError) from logs.views import addlogmsg from webvirtcloud.middleware import error_text @@ -884,10 +883,10 @@ def resize_memory(request, pk): new_cur_memory_custom = request.POST.get("cur_memory_custom", "") if new_cur_memory_custom: new_cur_memory = new_cur_memory_custom - # The quota charges the maximum. A running VM keeps it (resize_mem changes + # The quota charges the maximum. An active VM keeps it (resize_mem changes # only the current memory there); pass the current maximum, so a VM that # shuts down meanwhile does not get an unchecked one. - if instance.proxy.get_status() == VIR_DOMAIN_RUNNING: + if instance.proxy.instance.isActive(): new_memory = memory if not _is_int(new_memory) or not _is_int(new_cur_memory): messages.error(request, _("Enter a whole number for memory")) diff --git a/vrtManager/instance.py b/vrtManager/instance.py index af630eb7..32f53373 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -15,7 +15,6 @@ try: VIR_DOMAIN_AFFECT_LIVE, VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT, VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP, - VIR_DOMAIN_RUNNING, VIR_DOMAIN_XML_SECURE, VIR_DOMAIN_XML_INACTIVE, VIR_DOMAIN_XML_MIGRATABLE, @@ -1431,8 +1430,9 @@ class wvmInstance(wvmConnect): """ memory = int(memory) * 1024 cur_memory = int(cur_memory) * 1024 - # if dom is running change only ram - if self.get_status() == VIR_DOMAIN_RUNNING: + # an active VM (running, paused, ...) changes only its current memory; + # a paused guest's balloon driver applies it once it is resumed + if self.instance.isActive(): self.set_memory(cur_memory, VIR_DOMAIN_AFFECT_LIVE) self.set_memory(cur_memory, VIR_DOMAIN_AFFECT_CONFIG) return diff --git a/vrtManager/test_device_edits.py b/vrtManager/test_device_edits.py index a0e8605d..6bd15652 100644 --- a/vrtManager/test_device_edits.py +++ b/vrtManager/test_device_edits.py @@ -1,6 +1,7 @@ """Device edits of a VM: each definition is changed as it describes the device, every NIC type can be a boot device, posted values stay values, and -definition changes go through _defineXML.""" +definition changes go through _defineXML. An active VM's memory is resized +by balloon, a stopped one's in its definition.""" import unittest from unittest.mock import MagicMock @@ -106,5 +107,25 @@ class DeviceEditsTestCase(unittest.TestCase): self.assertIsNone(etree.fromstring(xml).find("model")) +class ResizeMemTestCase(unittest.TestCase): + def test_an_active_vm_changes_only_its_current_memory(self): + # a paused VM is active too: its guest applies the balloon once resumed + p = proxy(active=True) + p.resize_mem(96, 160) + self.assertEqual( + [c.args for c in p.instance.setMemoryFlags.call_args_list], + [(96 * 1024, VIR_DOMAIN_AFFECT_LIVE), (96 * 1024, VIR_DOMAIN_AFFECT_CONFIG)], + ) + p._defineXML.assert_not_called() + + def test_a_stopped_vm_gets_both_values_in_its_definition(self): + p = proxy(active=False) + p._XMLDesc = lambda flags: "131072131072" + p.resize_mem(96, 160) + tree = defined(p) + self.assertEqual((tree.findtext("memory"), tree.findtext("currentMemory")), (str(160 * 1024), str(96 * 1024))) + p.instance.setMemoryFlags.assert_not_called() + + if __name__ == "__main__": unittest.main()