From 87560dcf438f9b42052126782ea6e11742d3b605 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 11 Jun 2026 15:45:38 +0200 Subject: [PATCH 01/10] Report vcpu in stats API --- qubes/api/admin.py | 1 + qubes/app.py | 12 ++++++++---- qubes/tests/api_admin.py | 27 ++++++++++++++++++--------- qubes/tests/app.py | 18 ++++++++++++------ 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 6a7f0c176..218fc9c32 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -2539,6 +2539,7 @@ def _send_stats_single( cpu_time=int(vm_info["cpu_time"] / 1000000), cpu_usage=int(vm_info["cpu_usage"]), cpu_usage_raw=int(vm_info["cpu_usage_raw"]), + online_vcpus=int(vm_info["online_vcpus"]), ) return info_time, info diff --git a/qubes/app.py b/qubes/app.py index d9699654f..cd87206e7 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -384,10 +384,14 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): Return a tuple of (measurements_time, measurements), where measurements is a dictionary with key: domid, value: dict: + - memory_kb - current memory assigned, in kb + - online_vcpus - amount of vcpus assigned - cpu_time - absolute CPU usage (seconds since its startup) - - cpu_usage_raw - CPU usage in % - cpu_usage - CPU usage in % (normalized to number of vcpus) - - memory_kb - current memory assigned, in kb + + Future key(s) deprecation: + + - cpu_usage_raw - CPU usage in % This function requires Xen hypervisor. @@ -429,8 +433,8 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): domid = vm["domid"] current[domid] = {} current[domid]["memory_kb"] = vm["mem_kb"] + current[domid]["online_vcpus"] = max(vm["online_vcpus"], 1) current[domid]["cpu_time"] = round(vm["cpu_time"]) - vcpus = max(vm["online_vcpus"], 1) if domid in previous: current[domid]["cpu_usage_raw"] = round( (current[domid]["cpu_time"] - previous[domid]["cpu_time"]) @@ -444,7 +448,7 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): else: current[domid]["cpu_usage_raw"] = 0 current[domid]["cpu_usage"] = round( - current[domid]["cpu_usage_raw"] / vcpus + current[domid]["cpu_usage_raw"] / current[domid]["online_vcpus"] ) return current_time, current diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index cdf2f96e8..0ec306cc5 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -4077,16 +4077,18 @@ def test_630_vm_stats(self): stats1 = { 0: { + "memory_kb": 3733212, "cpu_time": 243951379111104 // 8, "cpu_usage": 0, "cpu_usage_raw": 0, - "memory_kb": 3733212, + "online_vcpus": 16, }, 1: { + "memory_kb": 303916, "cpu_time": 2849496569205, "cpu_usage": 0, "cpu_usage_raw": 0, - "memory_kb": 303916, + "online_vcpus": 2, }, } stats2 = copy.deepcopy(stats1) @@ -4149,34 +4151,38 @@ def name(self): unittest.mock.call( "dom0", "vm-stats", + memory_kb=stats1[0]["memory_kb"], cpu_time=stats1[0]["cpu_time"] // 1000000, cpu_usage=stats1[0]["cpu_usage"], cpu_usage_raw=stats1[0]["cpu_usage_raw"], - memory_kb=stats1[0]["memory_kb"], + online_vcpus=stats1[0]["online_vcpus"], ), unittest.mock.call( "test-template", "vm-stats", + memory_kb=stats1[1]["memory_kb"], cpu_time=stats1[1]["cpu_time"] // 1000000, cpu_usage=stats1[1]["cpu_usage"], cpu_usage_raw=stats1[1]["cpu_usage_raw"], - memory_kb=stats1[1]["memory_kb"], + online_vcpus=stats1[1]["online_vcpus"], ), unittest.mock.call( "dom0", "vm-stats", + memory_kb=stats2[0]["memory_kb"], cpu_time=stats2[0]["cpu_time"] // 1000000, cpu_usage=stats2[0]["cpu_usage"], cpu_usage_raw=stats2[0]["cpu_usage_raw"], - memory_kb=stats2[0]["memory_kb"], + online_vcpus=stats2[0]["online_vcpus"], ), unittest.mock.call( "test-template", "vm-stats", + memory_kb=stats2[1]["memory_kb"], cpu_time=stats2[1]["cpu_time"] // 1000000, cpu_usage=stats2[1]["cpu_usage"], cpu_usage_raw=stats2[1]["cpu_usage_raw"], - memory_kb=stats2[1]["memory_kb"], + online_vcpus=stats2[1]["online_vcpus"], ), ], ) @@ -4186,10 +4192,11 @@ def test_631_vm_stats_single_vm(self): stats1 = { 2: { + "memory_kb": 303916, "cpu_time": 2849496569205, "cpu_usage": 0, "cpu_usage_raw": 0, - "memory_kb": 303916, + "online_vcpus": 2, }, } stats2 = copy.deepcopy(stats1) @@ -4249,18 +4256,20 @@ def name(self): unittest.mock.call( "test-vm1", "vm-stats", + memory_kb=stats1[2]["memory_kb"], cpu_time=stats1[2]["cpu_time"] // 1000000, cpu_usage=stats1[2]["cpu_usage"], cpu_usage_raw=stats1[2]["cpu_usage_raw"], - memory_kb=stats1[2]["memory_kb"], + online_vcpus=stats1[2]["online_vcpus"], ), unittest.mock.call( "test-vm1", "vm-stats", + memory_kb=stats2[2]["memory_kb"], cpu_time=stats2[2]["cpu_time"] // 1000000, cpu_usage=stats2[2]["cpu_usage"], cpu_usage_raw=stats2[2]["cpu_usage_raw"], - memory_kb=stats2[2]["memory_kb"], + online_vcpus=stats2[2]["online_vcpus"], ), ], ) diff --git a/qubes/tests/app.py b/qubes/tests/app.py index 55a6e5091..e81ae0029 100644 --- a/qubes/tests/app.py +++ b/qubes/tests/app.py @@ -159,22 +159,25 @@ def test_000_get_vm_stats_single(self): self.assertIsNotNone(info_time) expected_info = { 0: { + "memory_kb": 3733212, "cpu_time": 243951379111104, "cpu_usage": 0, "cpu_usage_raw": 0, - "memory_kb": 3733212, + "online_vcpus": 8, }, 1: { + "memory_kb": 303916, "cpu_time": 2849496569205, "cpu_usage": 0, "cpu_usage_raw": 0, - "memory_kb": 303916, + "online_vcpus": 1, }, 11: { + "memory_kb": 3782668, "cpu_time": 249658663079978, "cpu_usage": 0, "cpu_usage_raw": 0, - "memory_kb": 3782668, + "online_vcpus": 8, }, } self.assertEqual(info, expected_info) @@ -193,22 +196,25 @@ def test_001_get_vm_stats_twice(self): self.assertIsNotNone(info_time) expected_info = { 0: { + "memory_kb": 3733212, "cpu_time": 243951379111104, "cpu_usage": 10, "cpu_usage_raw": 80, - "memory_kb": 3733212, + "online_vcpus": 8, }, 1: { + "memory_kb": 303916, "cpu_time": 2849496569205, "cpu_usage": 100, "cpu_usage_raw": 100, - "memory_kb": 303916, + "online_vcpus": 1, }, 11: { + "memory_kb": 3782668, "cpu_time": 249658663079978, "cpu_usage": 12, "cpu_usage_raw": 100, - "memory_kb": 3782668, + "online_vcpus": 8, }, } self.assertEqual(info, expected_info) From eac4592f821c19931b15c8668f6764508c369bab Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 11 Jun 2026 19:20:17 +0200 Subject: [PATCH 02/10] Broadcast memory correctly - Only meminfo reports the correct memory used for each balanced domain - Including overhead can be used to calculate how much memory is reserved for all qubes - Excluding overhead, just memory available in the qube on memory-balanced qubes, indicates memory that it can possibly get. For: https://github.com/qubesos/qubes-issues/issues/8368 --- qubes/api/admin.py | 31 +-- qubes/app.py | 143 ++++++++++- qubes/tests/api_admin.py | 117 ++++++--- qubes/tests/app.py | 508 ++++++++++++++++++++++--------------- qubes/tests/vm/__init__.py | 1 + qubes/vm/adminvm.py | 7 + qubes/vm/qubesvm.py | 13 +- 7 files changed, 548 insertions(+), 272 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 218fc9c32..8eb3767d2 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -2494,16 +2494,13 @@ async def backup_info(self): backup = await self._load_backup_profile(self.arg, skip_passphrase=True) return backup.get_backup_summary() - def _send_stats_single( - self, info_time, info, only_vm, filters, id_to_name_map - ): + def _send_stats_single(self, info_time, info, only_vm, filters): """A single iteration of sending VM stats :param info_time: time of previous iteration :param info: information retrieved in previous iteration :param only_vm: send information only about this VM :param filters: filters to apply on stats before sending - :param id_to_name_map: ID->VM name map, may be modified :return: tuple(info_time, info) - new information (to be passed to the next iteration) """ @@ -2511,24 +2508,10 @@ def _send_stats_single( info_time, info = self.app.host.get_vm_stats( info_time, info, only_vm=only_vm ) - for vm_id, vm_info in info.items(): - if vm_id not in id_to_name_map: - try: - name = self.app.vmm.libvirt_conn.lookupByID(vm_id).name() - except libvirt.libvirtError as err: - if err.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: - # stubdomain or so - name = None - else: - raise - id_to_name_map[vm_id] = name - else: - name = id_to_name_map[vm_id] - - # skip VMs with unknown name - if name is None: + for vm_info in info.values(): + if vm_info["is_stubdom"]: continue - + name = vm_info["name"] if not list(qubes.api.apply_filters([name], filters)): continue @@ -2536,6 +2519,9 @@ def _send_stats_single( name, "vm-stats", memory_kb=int(vm_info["memory_kb"]), + memory_assigned_usable=int(vm_info["memory_assigned_usable"]), + memory_assigned_total=int(vm_info["memory_assigned_total"]), + memory_with_swap_used=int(vm_info["memory_with_swap_used"]), cpu_time=int(vm_info["cpu_time"] / 1000000), cpu_usage=int(vm_info["cpu_usage"]), cpu_usage_raw=int(vm_info["cpu_usage_raw"]), @@ -2567,11 +2553,10 @@ async def vm_stats(self): info_time = None info = None - id_to_name_map = {0: "dom0"} try: while True: info_time, info = self._send_stats_single( - info_time, info, only_vm, stats_filters, id_to_name_map + info_time, info, only_vm, stats_filters ) await asyncio.sleep(self.app.stats_interval) except asyncio.CancelledError: diff --git a/qubes/app.py b/qubes/app.py index cd87206e7..8bb0ff2c9 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -67,6 +67,8 @@ import qubes.vm.qubesvm import qubes.vm.templatevm +import qubesdb + # pylint: enable=wrong-import-position @@ -384,14 +386,56 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): Return a tuple of (measurements_time, measurements), where measurements is a dictionary with key: domid, value: dict: - - memory_kb - current memory assigned, in kb - - online_vcpus - amount of vcpus assigned - - cpu_time - absolute CPU usage (seconds since its startup) - - cpu_usage - CPU usage in % (normalized to number of vcpus) + + - ``memory_assigned_usable``: + - Type: ``int``, unit: ``KiB``. + - Description: Amount of memory assigned to a qube available to + use. A qube cannot use more memory than this this value. On + memory balanced qubes, this value is volatile and controlled by + qmemman. + - Usage: This value should only be considered per qube. This value + should be compared with ``memory_with_swap_used``, to reflect how + much memory the qube is using from what it has. + - ``memory_assigned_total``: + - Type: ``int``, unit: ``KiB``. + - Description: Amount of memory assigned to a qube in total, this + includes ``memory_assigned_usable``, plus overhead of + videoram and whatever else the hypervisor considers. + - Usage: Summing the value of this key from all domains should + reflect the total amount of memory allocated to all qubes. + - ``memory_with_swap_used``: + - Type: ``int``, unit: ``KiB``. + - Description: Amount of memory the qube alleges to use, can be a + lie. On memory balanced qubes, it might include swap and might + differ up to 30MiB of the actual usage. As this value includes + swap and the qube can lie, it might be bigger than + ``memory_assigned_usable``. + - Usage: This value should be compared with + ``memory_assigned_usable``, to reflect how much memory the + qube is using from what it can use. + - ``online_vcpus`` + - Type: ``int``. + - Description: Amount of VCPUs assigned. + - ``cpu_time`` + - Type: ``int``, unit: ``nanosecond``, API broadcasts in + ``millisecond``. + - Description: Absolute CPU usage (since its startup). + - ``cpu_usage`` + - Type: ``int``, percentage. + - Description: CPU usage in normalized to the number of VCPUs. Will + be deprecated as it can be calculated on the client. Future key(s) deprecation: - - cpu_usage_raw - CPU usage in % + - ``memory_kb``: + - Type: ``int``, Unit: ``KiB`` + - Description: Amount memory assigned that is usable in the qube. + Will be deprecated in a future release due to its ambiguous name. + Prefer the equivalent ``memory_assigned_usable``. + - ``cpu_usage_raw``: + - Type: ``int``, percentage. + - Description: CPU usage. Will be deprecated as it can be calculated + on the client. This function requires Xen hypervisor. @@ -428,11 +472,77 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): raise qubes.exc.QubesVMNotRunningError(only_vm) else: info = self.app.vmm.xc.domain_getinfo(0, 1024) + + def query_stubdom_xid(domid) -> int: + stubdom_xid_str = self.app.vmm.xs.read( + "", + f"/local/domain/{domid}/image/device-model-domid", + ) + if stubdom_xid_str is not None and stubdom_xid_str.isdigit(): + return int(stubdom_xid_str) + return -1 + # TODO: add stubdomain stats to actual VMs for vm in info: domid = vm["domid"] current[domid] = {} - current[domid]["memory_kb"] = vm["mem_kb"] + stubdom_xid = -1 + + for key in ["name", "is_stubdom"]: + if key in vm: + current[domid][key] = vm[key] + + if domid in previous: + current[domid]["name"] = previous[domid]["name"] + current[domid]["is_stubdom"] = previous[domid]["is_stubdom"] + current[domid]["stubdom_xid"] = previous[domid]["stubdom_xid"] + stubdom_xid = current[domid]["stubdom_xid"] + else: + name = self.app.get_name_from_domid(domid) + if name == "Domain-0": + name = "dom0" + current[domid]["name"] = name + if self.app.vmm.is_xen and name.endswith("-dm"): + # or check for any value on : /local/domain/ID/device-model + current[domid]["is_stubdom"] = True + else: + current[domid]["is_stubdom"] = False + if self.app.vmm.is_xen and vm["hvm"]: + stubdom_xid = query_stubdom_xid(domid) + + current[domid]["stubdom_xid"] = stubdom_xid + assert "name" in current[domid] + assert "is_stubdom" in current[domid] + assert "stubdom_xid" in current[domid] + + if current[domid]["is_stubdom"]: + current[domid]["cpu_time"] = vm["cpu_time"] + continue + + current[domid]["memory_assigned_total"] = vm["maxmem_kb"] + current[domid]["memory_assigned_usable"] = vm["mem_kb"] + + current[domid]["memory_with_swap_used"] = current[domid][ + "memory_assigned_usable" + ] + current[domid]["memory_kb"] = current[domid][ + "memory_assigned_usable" + ] + + if self.app.vmm.is_xen and not current[domid]["is_stubdom"]: + untrusted_meminfo = self.app.vmm.xs.read( + "", f"/local/domain/{domid}/memory/meminfo" + ) + if ( + untrusted_meminfo is not None + and untrusted_meminfo.isdigit() + ): + meminfo = int(untrusted_meminfo) + del untrusted_meminfo + current[domid]["memory_with_swap_used"] = meminfo + else: + del untrusted_meminfo + current[domid]["online_vcpus"] = max(vm["online_vcpus"], 1) current[domid]["cpu_time"] = round(vm["cpu_time"]) if domid in previous: @@ -1890,3 +2000,24 @@ def on_property_pre_set_default_kernel( ): # pylint: disable=unused-argument validate_kernel(self, "default_kernel", newvalue) + + def get_name_from_domid(self, domid) -> str: + """ + Get qube that has a domid controlled by the VMM. + + The ``domid`` is not the same as ``QubesVM.qid``, it is the same as + ``QubesVM.xid``, which is ``QubesVM.libvirt_domain.ID()``. + + Implementation specific methods are used so internal domains can be + shown, in case of Xen, it is able to query device model stub domains. + + Returns the domain name. + """ + if self.vmm.is_xen: + name = self.vmm.xs.read("", "/local/domain/{}/name".format(domid)) + if name is None: + raise ValueError + name = name.decode() + else: + name = self.vmm.libvirt_conn.lookupByID(domid).name() + return name diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 0ec306cc5..146c8cb27 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -4077,14 +4077,24 @@ def test_630_vm_stats(self): stats1 = { 0: { + "name": "dom0", + "is_stubdom": False, "memory_kb": 3733212, + "memory_assigned_total": 3733244, + "memory_assigned_usable": 3733212, + "memory_with_swap_used": 3733212, "cpu_time": 243951379111104 // 8, "cpu_usage": 0, "cpu_usage_raw": 0, "online_vcpus": 16, }, 1: { + "name": "test-template", + "is_stubdom": False, "memory_kb": 303916, + "memory_assigned_total": 303932, + "memory_assigned_usable": 303916, + "memory_with_swap_used": 303916, "cpu_time": 2849496569205, "cpu_usage": 0, "cpu_usage_raw": 0, @@ -4144,55 +4154,72 @@ def name(self): unittest.mock.call(0, stats1, only_vm=None), ], ) - self.assertEqual( - send_event.mock_calls, - [ - unittest.mock.call(self.app, "connection-established"), - unittest.mock.call( - "dom0", - "vm-stats", - memory_kb=stats1[0]["memory_kb"], - cpu_time=stats1[0]["cpu_time"] // 1000000, - cpu_usage=stats1[0]["cpu_usage"], - cpu_usage_raw=stats1[0]["cpu_usage_raw"], - online_vcpus=stats1[0]["online_vcpus"], - ), - unittest.mock.call( - "test-template", - "vm-stats", - memory_kb=stats1[1]["memory_kb"], - cpu_time=stats1[1]["cpu_time"] // 1000000, - cpu_usage=stats1[1]["cpu_usage"], - cpu_usage_raw=stats1[1]["cpu_usage_raw"], - online_vcpus=stats1[1]["online_vcpus"], - ), - unittest.mock.call( - "dom0", - "vm-stats", - memory_kb=stats2[0]["memory_kb"], - cpu_time=stats2[0]["cpu_time"] // 1000000, - cpu_usage=stats2[0]["cpu_usage"], - cpu_usage_raw=stats2[0]["cpu_usage_raw"], - online_vcpus=stats2[0]["online_vcpus"], - ), - unittest.mock.call( - "test-template", - "vm-stats", - memory_kb=stats2[1]["memory_kb"], - cpu_time=stats2[1]["cpu_time"] // 1000000, - cpu_usage=stats2[1]["cpu_usage"], - cpu_usage_raw=stats2[1]["cpu_usage_raw"], - online_vcpus=stats2[1]["online_vcpus"], - ), - ], - ) + + expected = [ + unittest.mock.call(self.app, "connection-established"), + unittest.mock.call( + "dom0", + "vm-stats", + memory_kb=stats1[0]["memory_kb"], + memory_assigned_total=stats1[0]["memory_assigned_total"], + memory_assigned_usable=stats1[0]["memory_assigned_usable"], + memory_with_swap_used=stats1[0]["memory_with_swap_used"], + cpu_time=stats1[0]["cpu_time"] // 1000000, + cpu_usage=stats1[0]["cpu_usage"], + cpu_usage_raw=stats1[0]["cpu_usage_raw"], + online_vcpus=stats1[0]["online_vcpus"], + ), + unittest.mock.call( + "test-template", + "vm-stats", + memory_kb=stats1[1]["memory_kb"], + memory_assigned_total=stats1[1]["memory_assigned_total"], + memory_assigned_usable=stats1[1]["memory_assigned_usable"], + memory_with_swap_used=stats1[1]["memory_with_swap_used"], + cpu_time=stats1[1]["cpu_time"] // 1000000, + cpu_usage=stats1[1]["cpu_usage"], + cpu_usage_raw=stats1[1]["cpu_usage_raw"], + online_vcpus=stats1[1]["online_vcpus"], + ), + unittest.mock.call( + "dom0", + "vm-stats", + memory_kb=stats2[0]["memory_kb"], + memory_assigned_total=stats2[0]["memory_assigned_total"], + memory_assigned_usable=stats2[0]["memory_assigned_usable"], + memory_with_swap_used=stats2[0]["memory_with_swap_used"], + cpu_time=stats2[0]["cpu_time"] // 1000000, + cpu_usage=stats2[0]["cpu_usage"], + cpu_usage_raw=stats2[0]["cpu_usage_raw"], + online_vcpus=stats2[0]["online_vcpus"], + ), + unittest.mock.call( + "test-template", + "vm-stats", + memory_kb=stats2[1]["memory_kb"], + memory_assigned_total=stats2[1]["memory_assigned_total"], + memory_assigned_usable=stats2[1]["memory_assigned_usable"], + memory_with_swap_used=stats2[1]["memory_with_swap_used"], + cpu_time=stats2[1]["cpu_time"] // 1000000, + cpu_usage=stats2[1]["cpu_usage"], + cpu_usage_raw=stats2[1]["cpu_usage_raw"], + online_vcpus=stats2[1]["online_vcpus"], + ), + ] + + self.assertEqual(send_event.mock_calls, expected) def test_631_vm_stats_single_vm(self): send_event = unittest.mock.Mock(spec=[]) stats1 = { 2: { + "name": "test-vm1", + "is_stubdom": False, "memory_kb": 303916, + "memory_assigned_usable": 303932, + "memory_assigned_total": 303916, + "memory_with_swap_used": 300000, "cpu_time": 2849496569205, "cpu_usage": 0, "cpu_usage_raw": 0, @@ -4257,6 +4284,9 @@ def name(self): "test-vm1", "vm-stats", memory_kb=stats1[2]["memory_kb"], + memory_assigned_total=stats1[2]["memory_assigned_total"], + memory_assigned_usable=stats1[2]["memory_assigned_usable"], + memory_with_swap_used=stats1[2]["memory_with_swap_used"], cpu_time=stats1[2]["cpu_time"] // 1000000, cpu_usage=stats1[2]["cpu_usage"], cpu_usage_raw=stats1[2]["cpu_usage_raw"], @@ -4266,6 +4296,9 @@ def name(self): "test-vm1", "vm-stats", memory_kb=stats2[2]["memory_kb"], + memory_assigned_total=stats2[2]["memory_assigned_total"], + memory_assigned_usable=stats2[2]["memory_assigned_usable"], + memory_with_swap_used=stats2[2]["memory_with_swap_used"], cpu_time=stats2[2]["cpu_time"] // 1000000, cpu_usage=stats2[2]["cpu_usage"], cpu_usage_raw=stats2[2]["cpu_usage_raw"], diff --git a/qubes/tests/app.py b/qubes/tests/app.py index e81ae0029..f610eb153 100644 --- a/qubes/tests/app.py +++ b/qubes/tests/app.py @@ -43,100 +43,6 @@ class TestApp(qubes.tests.TestEmitter): class TC_20_QubesHost(qubes.tests.QubesTestCase): - sample_xc_domain_getinfo = [ - { - "paused": 0, - "cpu_time": 243951379111104, - "ssidref": 0, - "hvm": 0, - "shutdown_reason": 255, - "dying": 0, - "mem_kb": 3733212, - "domid": 0, - "max_vcpu_id": 7, - "crashed": 0, - "running": 1, - "maxmem_kb": 3734236, - "shutdown": 0, - "online_vcpus": 8, - "handle": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "cpupool": 0, - "blocked": 0, - }, - { - "paused": 0, - "cpu_time": 2849496569205, - "ssidref": 0, - "hvm": 0, - "shutdown_reason": 255, - "dying": 0, - "mem_kb": 303916, - "domid": 1, - "max_vcpu_id": 0, - "crashed": 0, - "running": 0, - "maxmem_kb": 308224, - "shutdown": 0, - "online_vcpus": 1, - "handle": [ - 116, - 174, - 229, - 207, - 17, - 1, - 79, - 39, - 191, - 37, - 41, - 186, - 205, - 158, - 219, - 8, - ], - "cpupool": 0, - "blocked": 1, - }, - { - "paused": 0, - "cpu_time": 249658663079978, - "ssidref": 0, - "hvm": 0, - "shutdown_reason": 255, - "dying": 0, - "mem_kb": 3782668, - "domid": 11, - "max_vcpu_id": 7, - "crashed": 0, - "running": 0, - "maxmem_kb": 3783692, - "shutdown": 0, - "online_vcpus": 8, - "handle": [ - 169, - 95, - 55, - 127, - 140, - 94, - 79, - 220, - 186, - 210, - 117, - 5, - 148, - 11, - 185, - 206, - ], - "cpupool": 0, - "blocked": 1, - }, - ] - def setUp(self): super().setUp() self.app = TestApp() @@ -144,110 +50,6 @@ def setUp(self): self.qubes_host = qubes.app.QubesHost(self.app) self.maxDiff = None - def test_000_get_vm_stats_single(self): - self.app.vmm.configure_mock( - **{"xc.domain_getinfo.return_value": self.sample_xc_domain_getinfo} - ) - - info_time, info = self.qubes_host.get_vm_stats() - self.assertEqual( - self.app.vmm.mock_calls, - [ - ("xc.domain_getinfo", (0, 1024), {}), - ], - ) - self.assertIsNotNone(info_time) - expected_info = { - 0: { - "memory_kb": 3733212, - "cpu_time": 243951379111104, - "cpu_usage": 0, - "cpu_usage_raw": 0, - "online_vcpus": 8, - }, - 1: { - "memory_kb": 303916, - "cpu_time": 2849496569205, - "cpu_usage": 0, - "cpu_usage_raw": 0, - "online_vcpus": 1, - }, - 11: { - "memory_kb": 3782668, - "cpu_time": 249658663079978, - "cpu_usage": 0, - "cpu_usage_raw": 0, - "online_vcpus": 8, - }, - } - self.assertEqual(info, expected_info) - - def test_001_get_vm_stats_twice(self): - self.app.vmm.configure_mock( - **{"xc.domain_getinfo.return_value": self.sample_xc_domain_getinfo} - ) - - prev_time, prev_info = self.qubes_host.get_vm_stats() - prev_time -= 1 - prev_info[0]["cpu_time"] -= 8 * 10**8 # 0.8s - prev_info[1]["cpu_time"] -= 10**9 # 1s - prev_info[11]["cpu_time"] -= 10**9 # 1s - info_time, info = self.qubes_host.get_vm_stats(prev_time, prev_info) - self.assertIsNotNone(info_time) - expected_info = { - 0: { - "memory_kb": 3733212, - "cpu_time": 243951379111104, - "cpu_usage": 10, - "cpu_usage_raw": 80, - "online_vcpus": 8, - }, - 1: { - "memory_kb": 303916, - "cpu_time": 2849496569205, - "cpu_usage": 100, - "cpu_usage_raw": 100, - "online_vcpus": 1, - }, - 11: { - "memory_kb": 3782668, - "cpu_time": 249658663079978, - "cpu_usage": 12, - "cpu_usage_raw": 100, - "online_vcpus": 8, - }, - } - self.assertEqual(info, expected_info) - self.assertEqual( - self.app.vmm.mock_calls, - [ - ("xc.domain_getinfo", (0, 1024), {}), - ("xc.domain_getinfo", (0, 1024), {}), - ], - ) - - def test_002_get_vm_stats_one_vm(self): - self.app.vmm.configure_mock( - **{ - "xc.domain_getinfo.return_value": [ - self.sample_xc_domain_getinfo[1] - ] - } - ) - - vm = mock.Mock - vm.xid = 1 - vm.name = "somevm" - - info_time, _info = self.qubes_host.get_vm_stats(only_vm=vm) - self.assertIsNotNone(info_time) - self.assertEqual( - self.app.vmm.mock_calls, - [ - ("xc.domain_getinfo", (1, 1), {}), - ], - ) - def test_010_iommu_supported(self): self.app.vmm.configure_mock( **{ @@ -869,6 +671,100 @@ def test_200_load_pools(self): class TC_90_Qubes(qubes.tests.QubesTestCase): + sample_xc_domain_getinfo = [ + { + "paused": 0, + "cpu_time": 243951379111104, + "ssidref": 0, + "hvm": 0, + "shutdown_reason": 255, + "dying": 0, + "mem_kb": 3733212, + "domid": 0, + "max_vcpu_id": 7, + "crashed": 0, + "running": 1, + "maxmem_kb": 3734236, + "shutdown": 0, + "online_vcpus": 8, + "handle": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "cpupool": 0, + "blocked": 0, + }, + { + "paused": 0, + "cpu_time": 2849496569205, + "ssidref": 0, + "hvm": 0, + "shutdown_reason": 255, + "dying": 0, + "mem_kb": 303916, + "domid": 1, + "max_vcpu_id": 0, + "crashed": 0, + "running": 0, + "maxmem_kb": 308224, + "shutdown": 0, + "online_vcpus": 1, + "handle": [ + 116, + 174, + 229, + 207, + 17, + 1, + 79, + 39, + 191, + 37, + 41, + 186, + 205, + 158, + 219, + 8, + ], + "cpupool": 0, + "blocked": 1, + }, + { + "paused": 0, + "cpu_time": 249658663079978, + "ssidref": 0, + "hvm": 0, + "shutdown_reason": 255, + "dying": 0, + "mem_kb": 3782668, + "domid": 2, + "max_vcpu_id": 7, + "crashed": 0, + "running": 0, + "maxmem_kb": 3783692, + "shutdown": 0, + "online_vcpus": 8, + "handle": [ + 169, + 95, + 55, + 127, + 140, + 94, + 79, + 220, + 186, + 210, + 117, + 5, + 148, + 11, + 185, + 206, + ], + "cpupool": 0, + "blocked": 1, + }, + ] + def setUp(self): super().setUp() self.app = qubes.Qubes( @@ -902,6 +798,7 @@ def setUp(self): qube.template_for_dispvms = True self.app.save() self.emitter = qubes.tests.TestEmitter() + self.maxDiff = None def tearDown(self): try: @@ -921,6 +818,221 @@ def cleanup_qubes(self): except AttributeError: pass + @mock.patch("qubes.vm.adminvm.AdminVM.untrusted_qdb") + @mock.patch("qubes.vm.qubesvm.QubesVM.untrusted_qdb") + def test_000_get_vm_stats_single(self, mock_qubesdb, mock_adminvm_qubesdb): + names = { + 0: "Domain-0", + 1: self.app.domains[1].name, + 2: self.app.domains[2].name, + } + xenstore = { + "/local/domain/0/memory/meminfo": b"849924", + "/local/domain/1/memory/meminfo": b"849925", + "/local/domain/2/memory/meminfo": b"849926", + } + + qdb = {"/qubes-service/meminfo-writer": "1"} + test_qubesdb = TestQubesDB(data=qdb) + for obj in [mock_qubesdb, mock_adminvm_qubesdb]: + obj.write.side_effect = test_qubesdb.write + obj.rm.side_effect = test_qubesdb.rm + obj.read.side_effect = test_qubesdb.read + + self.app.get_name_from_domid = lambda domid: names[domid] + self.app.vmm = mock.Mock() + self.app.vmm.configure_mock( + **{ + "xc.domain_getinfo.return_value": self.sample_xc_domain_getinfo, + "is_xen.return_value": True, + "xs.read.side_effect": lambda _, path: xenstore[path], + } + ) + + info_time, info = self.app.host.get_vm_stats() + + self.assertEqual( + self.app.vmm.mock_calls, + [ + ("xc.domain_getinfo", (0, 1024)), + ("xs.read", ("", "/local/domain/0/memory/meminfo")), + ("xs.read", ("", "/local/domain/1/memory/meminfo")), + ("xs.read", ("", "/local/domain/2/memory/meminfo")), + ], + ) + self.assertIsNotNone(info_time) + expected_info = { + 0: { + "name": "dom0", + "is_stubdom": False, + "memory_assigned_total": 3734236, + "memory_assigned_usable": 3733212, + "memory_kb": 3733212, + "memory_with_swap_used": 849924, + "cpu_time": 243951379111104, + "cpu_usage": 0, + "cpu_usage_raw": 0, + "online_vcpus": 8, + "stubdom_xid": -1, + }, + 1: { + "name": names[1], + "is_stubdom": False, + "memory_assigned_total": 308224, + "memory_assigned_usable": 303916, + "memory_kb": 303916, + "memory_with_swap_used": 849925, + "cpu_time": 2849496569205, + "cpu_usage": 0, + "cpu_usage_raw": 0, + "online_vcpus": 1, + "stubdom_xid": -1, + }, + 2: { + "name": names[2], + "is_stubdom": False, + "memory_assigned_total": 3783692, + "memory_assigned_usable": 3782668, + "memory_kb": 3782668, + "memory_with_swap_used": 849926, + "cpu_time": 249658663079978, + "cpu_usage": 0, + "cpu_usage_raw": 0, + "online_vcpus": 8, + "stubdom_xid": -1, + }, + } + self.assertEqual(info, expected_info) + + @mock.patch("qubes.vm.adminvm.AdminVM.untrusted_qdb") + @mock.patch("qubes.vm.qubesvm.QubesVM.untrusted_qdb") + def test_001_get_vm_stats_twice(self, mock_qubesdb, mock_adminvm_qubesdb): + names = { + 0: "Domain-0", + 1: self.app.domains[1].name, + 2: self.app.domains[2].name, + } + + xenstore = { + "/local/domain/0/memory/meminfo": b"849924", + "/local/domain/1/memory/meminfo": b"849925", + "/local/domain/2/memory/meminfo": b"849926", + } + self.app.get_name_from_domid = lambda domid: names[domid] + self.app.vmm = mock.Mock() + self.app.vmm.configure_mock( + **{ + "xc.domain_getinfo.return_value": self.sample_xc_domain_getinfo, + "is_xen.return_value": True, + "xs.read.side_effect": lambda _, path: xenstore[path], + } + ) + self.maxDiff = None + + qdb = {"/qubes-service/meminfo-writer": "1"} + test_qubesdb = TestQubesDB(data=qdb) + for obj in [mock_qubesdb, mock_adminvm_qubesdb]: + obj.write.side_effect = test_qubesdb.write + obj.rm.side_effect = test_qubesdb.rm + obj.read.side_effect = test_qubesdb.read + + prev_time, prev_info = self.app.host.get_vm_stats() + + prev_time -= 1 + prev_info[0]["cpu_time"] -= 8 * 10**8 # 0.8s + prev_info[1]["cpu_time"] -= 10**9 # 1s + prev_info[2]["cpu_time"] -= 10**9 # 1s + + info_time, info = self.app.host.get_vm_stats(prev_time, prev_info) + + self.assertIsNotNone(info_time) + expected_info = { + 0: { + "name": "dom0", + "is_stubdom": False, + "memory_assigned_total": 3734236, + "memory_assigned_usable": 3733212, + "memory_kb": 3733212, + "memory_with_swap_used": 849924, + "cpu_time": 243951379111104, + "cpu_usage": 10, + "cpu_usage_raw": 80, + "online_vcpus": 8, + "stubdom_xid": -1, + }, + 1: { + "name": names[1], + "is_stubdom": False, + "memory_assigned_total": 308224, + "memory_assigned_usable": 303916, + "memory_kb": 303916, + "memory_with_swap_used": 849925, + "cpu_time": 2849496569205, + "cpu_usage": 100, + "cpu_usage_raw": 100, + "online_vcpus": 1, + "stubdom_xid": -1, + }, + 2: { + "name": names[2], + "is_stubdom": False, + "memory_assigned_total": 3783692, + "memory_assigned_usable": 3782668, + "memory_kb": 3782668, + "memory_with_swap_used": 849926, + "cpu_time": 249658663079978, + "cpu_usage": 12, + "cpu_usage_raw": 100, + "online_vcpus": 8, + "stubdom_xid": -1, + }, + } + self.assertEqual(info, expected_info) + + self.assertEqual( + self.app.vmm.mock_calls, + [ + ("xc.domain_getinfo", (0, 1024), {}), + ("xs.read", ("", "/local/domain/0/memory/meminfo")), + ("xs.read", ("", "/local/domain/1/memory/meminfo")), + ("xs.read", ("", "/local/domain/2/memory/meminfo")), + ("xc.domain_getinfo", (0, 1024), {}), + ("xs.read", ("", "/local/domain/0/memory/meminfo")), + ("xs.read", ("", "/local/domain/1/memory/meminfo")), + ("xs.read", ("", "/local/domain/2/memory/meminfo")), + ], + ) + + def test_002_get_vm_stats_one_vm(self): + vm = mock.Mock + vm.xid = 1 + vm.name = "somevm" + + names = {1: vm.name} + xenstore = {"/local/domain/1/memory/meminfo": None} + self.app.get_name_from_domid = lambda domid: names[domid] + self.app.vmm = mock.Mock() + self.app.vmm.configure_mock( + **{ + "xc.domain_getinfo.return_value": [ + self.sample_xc_domain_getinfo[1] + ], + "is_xen.return_value": True, + "xs.read.side_effect": lambda _, path: xenstore[path], + } + ) + + info_time, _info = self.app.host.get_vm_stats(only_vm=vm) + self.assertIsNotNone(info_time) + + self.assertEqual( + self.app.vmm.mock_calls, + [ + ("xc.domain_getinfo", (1, 1)), + ("xs.read", ("", "/local/domain/1/memory/meminfo")), + ], + ) + def test_100_clockvm(self): appvm = self.app.add_new_vm( "AppVM", name="test-vm", template=self.template, label="red" diff --git a/qubes/tests/vm/__init__.py b/qubes/tests/vm/__init__.py index 956104a4a..409b73b36 100644 --- a/qubes/tests/vm/__init__.py +++ b/qubes/tests/vm/__init__.py @@ -30,6 +30,7 @@ class TestVMM(object): def __init__(self, offline_mode=False): self.offline_mode = offline_mode self.xs = unittest.mock.Mock() + self.is_xen = unittest.mock.Mock(return_value=True) self.libvirt_mock = unittest.mock.Mock() @property diff --git a/qubes/vm/adminvm.py b/qubes/vm/adminvm.py index 80acbbbfb..930481c7e 100644 --- a/qubes/vm/adminvm.py +++ b/qubes/vm/adminvm.py @@ -362,6 +362,13 @@ async def run_service_for_stdio(self, *args, input=None, **kwargs): return stdouterr + def is_memory_balancing_possible(self): + """Check if memory balancing can be enabled. + """ + if not self.app.vmm.is_xen: + return False + return True + @qubes.events.handler("domain-feature-pre-set:preload-dispvm-threshold") def on_feature_pre_set_preload_dispvm_threshold( self, event, feature, value, oldvalue=None diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index b1f1b3007..6d6c048b4 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -1093,11 +1093,14 @@ def block_devices(self): @property def untrusted_qdb(self): """QubesDB handle for this domain.""" - if self._qdb_connection is None: - if self.is_running(): - import qubesdb # pylint: disable=import-error + if self._qdb_connection is None and self.is_running(): + import qubesdb # pylint: disable=import-error + try: self._qdb_connection = qubesdb.QubesDB(self.name) + except qubesdb.Error: + # Methods might try to access the qubesdb before it is ready. + return None return self._qdb_connection @property @@ -2011,6 +2014,8 @@ def is_memory_balancing_possible(self): include the balloon driver) and lack of qrexec/meminfo-writer service support (no qubes tools installed). """ + if not self.app.vmm.is_xen: + return False if list(self.devices["pci"].get_assigned_devices()): return False if self.virt_mode == "hvm": @@ -2079,6 +2084,8 @@ def use_memory_hotplug(self): and reduces Xen's attack surface. This needs to be supported by the VM's kernel. """ + if not self.app.vmm.is_xen: + return False feature = self.features.check_with_template("memory-hotplug", None) if feature is not None: return bool(feature) From 40b897976c65a9326add2d8bf54ba740474deb64 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 25 Jun 2026 17:16:29 +0200 Subject: [PATCH 03/10] Report amount of swap used Although both values can be lied by the qube, we can distinguish the "memory types" in client tools. For: https://github.com/QubesOS/qubes-linux-utils/pull/140 --- qubes/api/admin.py | 29 +++++++++++++++------------ qubes/app.py | 42 ++++++++++++++++++++++++++++++++-------- qubes/tests/api_admin.py | 9 +++++++++ qubes/tests/app.py | 20 +++++++++++++++++++ qubes/vm/qubesvm.py | 9 +++++---- 5 files changed, 85 insertions(+), 24 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 8eb3767d2..50705fff1 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -2515,18 +2515,23 @@ def _send_stats_single(self, info_time, info, only_vm, filters): if not list(qubes.api.apply_filters([name], filters)): continue - self.send_event( - name, - "vm-stats", - memory_kb=int(vm_info["memory_kb"]), - memory_assigned_usable=int(vm_info["memory_assigned_usable"]), - memory_assigned_total=int(vm_info["memory_assigned_total"]), - memory_with_swap_used=int(vm_info["memory_with_swap_used"]), - cpu_time=int(vm_info["cpu_time"] / 1000000), - cpu_usage=int(vm_info["cpu_usage"]), - cpu_usage_raw=int(vm_info["cpu_usage_raw"]), - online_vcpus=int(vm_info["online_vcpus"]), - ) + data = { + "memory_kb": int(vm_info["memory_kb"]), + "memory_assigned_usable": int( + vm_info["memory_assigned_usable"] + ), + "memory_assigned__total": int(vm_info["memory_assigned_total"]), + "memory_with_swap_used": int(vm_info["memory_with_swap_used"]), + "cpu_time": int(vm_info["cpu_time"] / 1000000), + "cpu_usage": int(vm_info["cpu_usage"]), + "cpu_usage_raw": int(vm_info["cpu_usage_raw"]), + "online_vcpus": int(vm_info["online_vcpus"]), + } + + if "swap_used" in vm_info: + data["swap_used"] = int(vm_info["swap_used"]) + + self.send_event(name, "vm-stats", **data) return info_time, info diff --git a/qubes/app.py b/qubes/app.py index 8bb0ff2c9..7e77306cb 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -405,14 +405,24 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): reflect the total amount of memory allocated to all qubes. - ``memory_with_swap_used``: - Type: ``int``, unit: ``KiB``. - - Description: Amount of memory the qube alleges to use, can be a - lie. On memory balanced qubes, it might include swap and might - differ up to 30MiB of the actual usage. As this value includes - swap and the qube can lie, it might be bigger than - ``memory_assigned_usable``. + - Description: Amount of memory the qube alleges to use, including + swap, can be a lie. On memory balanced qubes, might differ up to + 30MiB of the actual usage. As this value includes swap and the + qube can lie, it might be bigger than ``memory_assigned_usable``. - Usage: This value should be compared with ``memory_assigned_usable``, to reflect how much memory the - qube is using from what it can use. + qube is using from what it can use. Deduct this value with + ``swap_used`` to reflect how much memory the qube is + using internally. + - ``swap_used``: + - Type: ``int``, unit: ``KiB``. + - Description: Amount of swap the qube alleges to use, can be a + lie. As this value can be lie, it might be bigger than + ``memory_assigned_usable``. This variable is not set if the + value of ``memory/swapinfo`` is not set or invalid, + - Usage: This value should be compared with + ``memory_with_swap_used`` to determine how much the qube is + swaping over what it is using of real memory. - ``online_vcpus`` - Type: ``int``. - Description: Amount of VCPUs assigned. @@ -437,6 +447,7 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): - Description: CPU usage. Will be deprecated as it can be calculated on the client. + This function requires Xen hypervisor. ..warning: @@ -522,10 +533,11 @@ def query_stubdom_xid(domid) -> int: current[domid]["memory_assigned_total"] = vm["maxmem_kb"] current[domid]["memory_assigned_usable"] = vm["mem_kb"] - current[domid]["memory_with_swap_used"] = current[domid][ + current[domid]["memory_kb"] = current[domid][ "memory_assigned_usable" ] - current[domid]["memory_kb"] = current[domid][ + + current[domid]["memory_with_swap_used"] = current[domid][ "memory_assigned_usable" ] @@ -540,6 +552,20 @@ def query_stubdom_xid(domid) -> int: meminfo = int(untrusted_meminfo) del untrusted_meminfo current[domid]["memory_with_swap_used"] = meminfo + # Only query swapinfo is meminfo is valid to avoid + # extraneous calls. + untrusted_swapinfo = self.app.vmm.xs.read( + "", f"/local/domain/{domid}/memory/swapinfo" + ) + if ( + untrusted_swapinfo is not None + and untrusted_swapinfo.isdigit() + ): + swapinfo = int(untrusted_swapinfo) + del untrusted_swapinfo + current[domid]["swap_used"] = swapinfo + else: + del untrusted_swapinfo else: del untrusted_meminfo diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 146c8cb27..0154187a6 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -4083,6 +4083,7 @@ def test_630_vm_stats(self): "memory_assigned_total": 3733244, "memory_assigned_usable": 3733212, "memory_with_swap_used": 3733212, + "swap_used": 0, "cpu_time": 243951379111104 // 8, "cpu_usage": 0, "cpu_usage_raw": 0, @@ -4095,6 +4096,7 @@ def test_630_vm_stats(self): "memory_assigned_total": 303932, "memory_assigned_usable": 303916, "memory_with_swap_used": 303916, + "swap_used": 0, "cpu_time": 2849496569205, "cpu_usage": 0, "cpu_usage_raw": 0, @@ -4164,6 +4166,7 @@ def name(self): memory_assigned_total=stats1[0]["memory_assigned_total"], memory_assigned_usable=stats1[0]["memory_assigned_usable"], memory_with_swap_used=stats1[0]["memory_with_swap_used"], + swap_used=stats1[0]["swap_used"], cpu_time=stats1[0]["cpu_time"] // 1000000, cpu_usage=stats1[0]["cpu_usage"], cpu_usage_raw=stats1[0]["cpu_usage_raw"], @@ -4176,6 +4179,7 @@ def name(self): memory_assigned_total=stats1[1]["memory_assigned_total"], memory_assigned_usable=stats1[1]["memory_assigned_usable"], memory_with_swap_used=stats1[1]["memory_with_swap_used"], + swap_used=stats1[1]["swap_used"], cpu_time=stats1[1]["cpu_time"] // 1000000, cpu_usage=stats1[1]["cpu_usage"], cpu_usage_raw=stats1[1]["cpu_usage_raw"], @@ -4188,6 +4192,7 @@ def name(self): memory_assigned_total=stats2[0]["memory_assigned_total"], memory_assigned_usable=stats2[0]["memory_assigned_usable"], memory_with_swap_used=stats2[0]["memory_with_swap_used"], + swap_used=stats2[0]["swap_used"], cpu_time=stats2[0]["cpu_time"] // 1000000, cpu_usage=stats2[0]["cpu_usage"], cpu_usage_raw=stats2[0]["cpu_usage_raw"], @@ -4200,6 +4205,7 @@ def name(self): memory_assigned_total=stats2[1]["memory_assigned_total"], memory_assigned_usable=stats2[1]["memory_assigned_usable"], memory_with_swap_used=stats2[1]["memory_with_swap_used"], + swap_used=stats2[1]["swap_used"], cpu_time=stats2[1]["cpu_time"] // 1000000, cpu_usage=stats2[1]["cpu_usage"], cpu_usage_raw=stats2[1]["cpu_usage_raw"], @@ -4220,6 +4226,7 @@ def test_631_vm_stats_single_vm(self): "memory_assigned_usable": 303932, "memory_assigned_total": 303916, "memory_with_swap_used": 300000, + "swap_used": 0, "cpu_time": 2849496569205, "cpu_usage": 0, "cpu_usage_raw": 0, @@ -4287,6 +4294,7 @@ def name(self): memory_assigned_total=stats1[2]["memory_assigned_total"], memory_assigned_usable=stats1[2]["memory_assigned_usable"], memory_with_swap_used=stats1[2]["memory_with_swap_used"], + swap_used=stats1[2]["swap_used"], cpu_time=stats1[2]["cpu_time"] // 1000000, cpu_usage=stats1[2]["cpu_usage"], cpu_usage_raw=stats1[2]["cpu_usage_raw"], @@ -4299,6 +4307,7 @@ def name(self): memory_assigned_total=stats2[2]["memory_assigned_total"], memory_assigned_usable=stats2[2]["memory_assigned_usable"], memory_with_swap_used=stats2[2]["memory_with_swap_used"], + swap_used=stats2[2]["swap_used"], cpu_time=stats2[2]["cpu_time"] // 1000000, cpu_usage=stats2[2]["cpu_usage"], cpu_usage_raw=stats2[2]["cpu_usage_raw"], diff --git a/qubes/tests/app.py b/qubes/tests/app.py index f610eb153..de41891a7 100644 --- a/qubes/tests/app.py +++ b/qubes/tests/app.py @@ -830,6 +830,9 @@ def test_000_get_vm_stats_single(self, mock_qubesdb, mock_adminvm_qubesdb): "/local/domain/0/memory/meminfo": b"849924", "/local/domain/1/memory/meminfo": b"849925", "/local/domain/2/memory/meminfo": b"849926", + "/local/domain/0/memory/swapinfo": None, + "/local/domain/1/memory/swapinfo": b"35", + "/local/domain/2/memory/swapinfo": b"4", } qdb = {"/qubes-service/meminfo-writer": "1"} @@ -856,8 +859,11 @@ def test_000_get_vm_stats_single(self, mock_qubesdb, mock_adminvm_qubesdb): [ ("xc.domain_getinfo", (0, 1024)), ("xs.read", ("", "/local/domain/0/memory/meminfo")), + ("xs.read", ("", "/local/domain/0/memory/swapinfo")), ("xs.read", ("", "/local/domain/1/memory/meminfo")), + ("xs.read", ("", "/local/domain/1/memory/swapinfo")), ("xs.read", ("", "/local/domain/2/memory/meminfo")), + ("xs.read", ("", "/local/domain/2/memory/swapinfo")), ], ) self.assertIsNotNone(info_time) @@ -882,6 +888,7 @@ def test_000_get_vm_stats_single(self, mock_qubesdb, mock_adminvm_qubesdb): "memory_assigned_usable": 303916, "memory_kb": 303916, "memory_with_swap_used": 849925, + "swap_used": 35, "cpu_time": 2849496569205, "cpu_usage": 0, "cpu_usage_raw": 0, @@ -895,6 +902,7 @@ def test_000_get_vm_stats_single(self, mock_qubesdb, mock_adminvm_qubesdb): "memory_assigned_usable": 3782668, "memory_kb": 3782668, "memory_with_swap_used": 849926, + "swap_used": 4, "cpu_time": 249658663079978, "cpu_usage": 0, "cpu_usage_raw": 0, @@ -917,6 +925,9 @@ def test_001_get_vm_stats_twice(self, mock_qubesdb, mock_adminvm_qubesdb): "/local/domain/0/memory/meminfo": b"849924", "/local/domain/1/memory/meminfo": b"849925", "/local/domain/2/memory/meminfo": b"849926", + "/local/domain/0/memory/swapinfo": b"0", + "/local/domain/1/memory/swapinfo": b"35", + "/local/domain/2/memory/swapinfo": b"4", } self.app.get_name_from_domid = lambda domid: names[domid] self.app.vmm = mock.Mock() @@ -954,6 +965,7 @@ def test_001_get_vm_stats_twice(self, mock_qubesdb, mock_adminvm_qubesdb): "memory_assigned_usable": 3733212, "memory_kb": 3733212, "memory_with_swap_used": 849924, + "swap_used": 0, "cpu_time": 243951379111104, "cpu_usage": 10, "cpu_usage_raw": 80, @@ -967,6 +979,7 @@ def test_001_get_vm_stats_twice(self, mock_qubesdb, mock_adminvm_qubesdb): "memory_assigned_usable": 303916, "memory_kb": 303916, "memory_with_swap_used": 849925, + "swap_used": 35, "cpu_time": 2849496569205, "cpu_usage": 100, "cpu_usage_raw": 100, @@ -980,6 +993,7 @@ def test_001_get_vm_stats_twice(self, mock_qubesdb, mock_adminvm_qubesdb): "memory_assigned_usable": 3782668, "memory_kb": 3782668, "memory_with_swap_used": 849926, + "swap_used": 4, "cpu_time": 249658663079978, "cpu_usage": 12, "cpu_usage_raw": 100, @@ -994,12 +1008,18 @@ def test_001_get_vm_stats_twice(self, mock_qubesdb, mock_adminvm_qubesdb): [ ("xc.domain_getinfo", (0, 1024), {}), ("xs.read", ("", "/local/domain/0/memory/meminfo")), + ("xs.read", ("", "/local/domain/0/memory/swapinfo")), ("xs.read", ("", "/local/domain/1/memory/meminfo")), + ("xs.read", ("", "/local/domain/1/memory/swapinfo")), ("xs.read", ("", "/local/domain/2/memory/meminfo")), + ("xs.read", ("", "/local/domain/2/memory/swapinfo")), ("xc.domain_getinfo", (0, 1024), {}), ("xs.read", ("", "/local/domain/0/memory/meminfo")), + ("xs.read", ("", "/local/domain/0/memory/swapinfo")), ("xs.read", ("", "/local/domain/1/memory/meminfo")), + ("xs.read", ("", "/local/domain/1/memory/swapinfo")), ("xs.read", ("", "/local/domain/2/memory/meminfo")), + ("xs.read", ("", "/local/domain/2/memory/swapinfo")), ], ) diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 6d6c048b4..b4db9c44b 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -2883,10 +2883,11 @@ def create_qdb_entries(self): # xenstore for it until decided otherwise if qmemman_present and self.maxmem: xs_basedir = f"/local/domain/{self.xid}" - self.app.vmm.xs.write("", f"{xs_basedir}/memory/meminfo", "") - self.app.vmm.xs.set_permissions( - "", f"{xs_basedir}/memory/meminfo", [{"dom": self.xid}] - ) + for key in ["meminfo", "swapinfo"]: + self.app.vmm.xs.write("", f"{xs_basedir}/memory/{key}", "") + self.app.vmm.xs.set_permissions( + "", f"{xs_basedir}/memory/{key}", [{"dom": self.xid}] + ) if self.use_memory_hotplug: self.app.vmm.xs.write( "", From 2303c6681a98716c8ec42e85a1e697693b00f57b Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Tue, 9 Jun 2026 13:52:06 +0200 Subject: [PATCH 04/10] Do not require Xen to get VM stats It's not completely Xen agnostic because then it would not be possible to query stubdomains, as libvirt is not aware of them. On the other hand, non-Xen has one more API methods working. When not using Xen, construct the dictionary as "xc.domain_getinfo()" would, so the info loop can consume from both inputs. Stubdomains are returned as separate keys instead of aggregated to allow clients to distinguish if it's HVM using the resources or it's stubdomain. This is specially relevant for HVMs that have memory balancing enabled. --- qubes/api/admin.py | 16 +++- qubes/app.py | 188 +++++++++++++++++++++++++++++++-------- qubes/tests/api_admin.py | 71 ++++++++------- 3 files changed, 202 insertions(+), 73 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 50705fff1..8a18c2b12 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -2517,10 +2517,10 @@ def _send_stats_single(self, info_time, info, only_vm, filters): data = { "memory_kb": int(vm_info["memory_kb"]), + "memory_assigned_total": int(vm_info["memory_assigned_total"]), "memory_assigned_usable": int( vm_info["memory_assigned_usable"] ), - "memory_assigned__total": int(vm_info["memory_assigned_total"]), "memory_with_swap_used": int(vm_info["memory_with_swap_used"]), "cpu_time": int(vm_info["cpu_time"] / 1000000), "cpu_usage": int(vm_info["cpu_usage"]), @@ -2528,8 +2528,18 @@ def _send_stats_single(self, info_time, info, only_vm, filters): "online_vcpus": int(vm_info["online_vcpus"]), } - if "swap_used" in vm_info: - data["swap_used"] = int(vm_info["swap_used"]) + optional = { + "swap_used": int, + "cpu_time_internal": lambda x: int(value / 1000000), + "cpu_usage_internal": lambda x: round(float(x), 1), + "online_vcpus_internal": int, + } + + for key, func in optional.items(): + if key not in vm_info: + continue + value = vm_info[key] + data[key] = func(value) self.send_event(name, "vm-stats", **data) diff --git a/qubes/app.py b/qubes/app.py index 7e77306cb..87594a178 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -67,8 +67,6 @@ import qubes.vm.qubesvm import qubes.vm.templatevm -import qubesdb - # pylint: enable=wrong-import-position @@ -400,7 +398,8 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): - Type: ``int``, unit: ``KiB``. - Description: Amount of memory assigned to a qube in total, this includes ``memory_assigned_usable``, plus overhead of - videoram and whatever else the hypervisor considers. + videoram and whatever else the hypervisor considers, such as + memory assigned to device model stub domains. - Usage: Summing the value of this key from all domains should reflect the total amount of memory allocated to all qubes. - ``memory_with_swap_used``: @@ -435,6 +434,22 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): - Description: CPU usage in normalized to the number of VCPUs. Will be deprecated as it can be calculated on the client. + Implementation specific (normally device model stub domains): + + - ``online_vcpus_internal``: + - Type: ``int`` + - Description: Same as description ``online_vcpus``, but only + consider internal usage. + - ``cpu_time_internal``: + - Type: ``int``, unit: ``nanosecond``, API broadcasts in + ``millisecond``. + - Description: Same description as ``cpu_time``, but only consider + internal usage. + - ``cpu_usage_internal``: + - Type: ``int``, percentage. + - Description: Same description as ``cpu_usage``, but only consider + internal usage. + Future key(s) deprecation: - ``memory_kb``: @@ -447,13 +462,12 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): - Description: CPU usage. Will be deprecated as it can be calculated on the client. - - This function requires Xen hypervisor. + This function requires Xen hypervisor for detailed overview. ..warning: This function may return info about implementation-specific VMs, - like stubdomains for HVM + like stubdomains for HVM, aggregated to the connected domain. :param previous: previous measurement :param previous_time: time of previous measurement @@ -461,6 +475,7 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): :raises NotImplementedError: when not under Xen """ + # pylint: disable=too-many-statements if (previous_time is None) != (previous is None): raise ValueError( @@ -472,17 +487,84 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): current_time = time.time() current = {} - if not self.app.vmm.is_xen: - raise NotImplementedError("This function requires Xen hypervisor") if only_vm: xid = only_vm.xid if xid < 0: raise qubes.exc.QubesVMNotRunningError(only_vm) - info = self.app.vmm.xc.domain_getinfo(xid, 1) - if info[0]["domid"] != xid: - raise qubes.exc.QubesVMNotRunningError(only_vm) + if self.app.vmm.is_xen: + if ( + only_vm_stubdom_xid := getattr(only_vm, "stubdom_xid", -1) + ) and only_vm_stubdom_xid > 0: + if only_vm_stubdom_xid == xid + 1: + # Avoid multiple domain_getinfo calls. + info = self.app.vmm.xc.domain_getinfo(xid, 2) + else: + info = self.app.vmm.xc.domain_getinfo(xid, 1) + stubdom_info = self.app.vmm.xc.domain_getinfo( + only_vm_stubdom_xid, 1 + )[0] + info.append(stubdom_info) + else: + info = self.app.vmm.xc.domain_getinfo(xid, 1) + if info[0]["domid"] != xid: + raise qubes.exc.QubesVMNotRunningError(only_vm) + else: + if not only_vm.libvirt_domain: + raise qubes.exc.QubesVMNotRunningError(only_vm) + dom_info = only_vm.libvirt_domain.info() + info = [ + { + "name": only_vm.name, + "domid": only_vm.xid, + "maxmem_kb": dom_info[1], + "mem_kb": dom_info[2], + "online_vcpus": dom_info[3], + "cpu_time": dom_info[4], + "is_stubdom": False, + "hvm": 0, + } + ] else: - info = self.app.vmm.xc.domain_getinfo(0, 1024) + if self.app.vmm.is_xen: + info = self.app.vmm.xc.domain_getinfo(0, 1024) + else: + running = self.app.vmm.libvirt_conn.listAllDomains( + libvirt.VIR_CONNECT_GET_ALL_DOMAINS_STATS_ACTIVE + ) + info = [] + for dom in running: + dom_info = dom.info() + dom_info_dict = { + "domid": dom.ID(), + "maxmem_kb": dom_info[1], + "mem_kb": dom_info[2], + "online_vcpus": dom_info[3], + "cpu_time": dom_info[4], + "is_stubdom": False, + "hvm": 0, + } + if dom_info_dict["domid"] not in previous: + dom_name = dom.name() + if dom_name == "Domain-0": + dom_name = "dom0" + dom_info_dict["name"] = dom_name + info.append(dom_info_dict) + + cpu_usage_raw_denominator = None + if previous_time: + time_diff = current_time - previous_time + cpu_usage_raw_denominator = 100 / time_diff / 1_000_000_000 + + def calculate_cpu_usage_raw(current_cpu_time, previous_cpu_time): + assert cpu_usage_raw_denominator + cpu_usage_raw = round( + (current_cpu_time - previous_cpu_time) + * cpu_usage_raw_denominator + ) + if cpu_usage_raw < 0: + # VM has been rebooted + return 0 + return cpu_usage_raw def query_stubdom_xid(domid) -> int: stubdom_xid_str = self.app.vmm.xs.read( @@ -493,50 +575,54 @@ def query_stubdom_xid(domid) -> int: return int(stubdom_xid_str) return -1 - # TODO: add stubdomain stats to actual VMs - for vm in info: - domid = vm["domid"] - current[domid] = {} + info = sorted(info, key=lambda domain: domain["domid"]) + info = {v["domid"]: v for v in info} + for domid in info.keys(): stubdom_xid = -1 + current[domid] = {} for key in ["name", "is_stubdom"]: - if key in vm: - current[domid][key] = vm[key] + if key in info[domid]: + current[domid][key] = info[domid][key] if domid in previous: current[domid]["name"] = previous[domid]["name"] current[domid]["is_stubdom"] = previous[domid]["is_stubdom"] current[domid]["stubdom_xid"] = previous[domid]["stubdom_xid"] stubdom_xid = current[domid]["stubdom_xid"] - else: + elif "name" not in current[domid]: name = self.app.get_name_from_domid(domid) if name == "Domain-0": name = "dom0" current[domid]["name"] = name if self.app.vmm.is_xen and name.endswith("-dm"): - # or check for any value on : /local/domain/ID/device-model + # or check for any value on: /local/domain/ID/device-model current[domid]["is_stubdom"] = True else: current[domid]["is_stubdom"] = False - if self.app.vmm.is_xen and vm["hvm"]: + if self.app.vmm.is_xen and info[domid]["hvm"]: stubdom_xid = query_stubdom_xid(domid) current[domid]["stubdom_xid"] = stubdom_xid assert "name" in current[domid] assert "is_stubdom" in current[domid] - assert "stubdom_xid" in current[domid] if current[domid]["is_stubdom"]: - current[domid]["cpu_time"] = vm["cpu_time"] + current[domid]["cpu_time"] = info[domid]["cpu_time"] continue - current[domid]["memory_assigned_total"] = vm["maxmem_kb"] - current[domid]["memory_assigned_usable"] = vm["mem_kb"] + current[domid]["memory_assigned_total"] = info[domid]["maxmem_kb"] + current[domid]["memory_assigned_usable"] = info[domid]["mem_kb"] current[domid]["memory_kb"] = current[domid][ "memory_assigned_usable" ] + if stubdom_xid > 0: + current[domid]["memory_assigned_total"] += info[stubdom_xid][ + "maxmem_kb" + ] + current[domid]["memory_with_swap_used"] = current[domid][ "memory_assigned_usable" ] @@ -569,23 +655,49 @@ def query_stubdom_xid(domid) -> int: else: del untrusted_meminfo - current[domid]["online_vcpus"] = max(vm["online_vcpus"], 1) - current[domid]["cpu_time"] = round(vm["cpu_time"]) + current[domid]["online_vcpus"] = info[domid]["online_vcpus"] + if stubdom_xid > 0: + current[domid]["online_vcpus_internal"] = info[stubdom_xid][ + "online_vcpus" + ] + + current[domid]["cpu_time"] = info[domid]["cpu_time"] + if stubdom_xid > 0: + current[domid]["cpu_time_internal"] = info[stubdom_xid][ + "cpu_time" + ] + if domid in previous: - current[domid]["cpu_usage_raw"] = round( - (current[domid]["cpu_time"] - previous[domid]["cpu_time"]) - / 1000**3 - * 100 - / (current_time - previous_time) + current[domid]["cpu_usage_raw"] = calculate_cpu_usage_raw( + current[domid]["cpu_time"], previous[domid]["cpu_time"] ) - if current[domid]["cpu_usage_raw"] < 0: - # VM has been rebooted - current[domid]["cpu_usage_raw"] = 0 + if stubdom_xid > 0: + current[domid]["cpu_usage_raw_internal"] = ( + calculate_cpu_usage_raw( + current[domid]["cpu_time_internal"], + previous[domid]["cpu_time_internal"], + ) + ) else: current[domid]["cpu_usage_raw"] = 0 - current[domid]["cpu_usage"] = round( - current[domid]["cpu_usage_raw"] / current[domid]["online_vcpus"] - ) + if stubdom_xid > 0: + current[domid]["cpu_usage_raw_internal"] = 0 + + if current[domid]["online_vcpus"] > 0: + current[domid]["cpu_usage"] = ( + current[domid]["cpu_usage_raw"] + // current[domid]["online_vcpus"] + ) + else: + current[domid]["cpu_usage"] = 0 + if stubdom_xid > 0: + if current[domid]["online_vcpus_internal"] > 0: + current[domid]["cpu_usage_internal"] = ( + current[domid]["cpu_usage_raw_internal"] + // current[domid]["online_vcpus_internal"] + ) + else: + current[domid]["cpu_usage_internal"] = 0 return current_time, current diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 0154187a6..f30ea9f1b 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -4231,6 +4231,9 @@ def test_631_vm_stats_single_vm(self): "cpu_usage": 0, "cpu_usage_raw": 0, "online_vcpus": 2, + "cpu_time_internal": 2849496569205, + "cpu_usage_internal": 0.0, + "online_vcpus_internal": 1, }, } stats2 = copy.deepcopy(stats1) @@ -4283,38 +4286,42 @@ def name(self): unittest.mock.call(0, stats1, only_vm=self.vm), ], ) - self.assertEqual( - send_event.mock_calls, - [ - unittest.mock.call(self.app, "connection-established"), - unittest.mock.call( - "test-vm1", - "vm-stats", - memory_kb=stats1[2]["memory_kb"], - memory_assigned_total=stats1[2]["memory_assigned_total"], - memory_assigned_usable=stats1[2]["memory_assigned_usable"], - memory_with_swap_used=stats1[2]["memory_with_swap_used"], - swap_used=stats1[2]["swap_used"], - cpu_time=stats1[2]["cpu_time"] // 1000000, - cpu_usage=stats1[2]["cpu_usage"], - cpu_usage_raw=stats1[2]["cpu_usage_raw"], - online_vcpus=stats1[2]["online_vcpus"], - ), - unittest.mock.call( - "test-vm1", - "vm-stats", - memory_kb=stats2[2]["memory_kb"], - memory_assigned_total=stats2[2]["memory_assigned_total"], - memory_assigned_usable=stats2[2]["memory_assigned_usable"], - memory_with_swap_used=stats2[2]["memory_with_swap_used"], - swap_used=stats2[2]["swap_used"], - cpu_time=stats2[2]["cpu_time"] // 1000000, - cpu_usage=stats2[2]["cpu_usage"], - cpu_usage_raw=stats2[2]["cpu_usage_raw"], - online_vcpus=stats2[2]["online_vcpus"], - ), - ], - ) + expected = [ + unittest.mock.call(self.app, "connection-established"), + unittest.mock.call( + "test-vm1", + "vm-stats", + memory_kb=stats1[2]["memory_kb"], + memory_assigned_total=stats1[2]["memory_assigned_total"], + memory_assigned_usable=stats1[2]["memory_assigned_usable"], + memory_with_swap_used=stats1[2]["memory_with_swap_used"], + swap_used=stats1[2]["swap_used"], + cpu_time=stats1[2]["cpu_time"] // 1000000, + cpu_usage=stats1[2]["cpu_usage"], + cpu_usage_raw=stats1[2]["cpu_usage_raw"], + online_vcpus=stats1[2]["online_vcpus"], + cpu_time_internal=stats1[2]["cpu_time_internal"] // 1000000, + cpu_usage_internal=stats1[2]["cpu_usage_internal"], + online_vcpus_internal=stats1[2]["online_vcpus_internal"], + ), + unittest.mock.call( + "test-vm1", + "vm-stats", + memory_kb=stats2[2]["memory_kb"], + memory_assigned_total=stats2[2]["memory_assigned_total"], + memory_assigned_usable=stats2[2]["memory_assigned_usable"], + memory_with_swap_used=stats2[2]["memory_with_swap_used"], + swap_used=stats2[2]["swap_used"], + cpu_time=stats2[2]["cpu_time"] // 1000000, + cpu_usage=stats2[2]["cpu_usage"], + cpu_usage_raw=stats2[2]["cpu_usage_raw"], + online_vcpus=stats2[2]["online_vcpus"], + cpu_time_internal=stats2[2]["cpu_time_internal"] // 1000000, + cpu_usage_internal=stats2[2]["cpu_usage_internal"], + online_vcpus_internal=stats2[2]["online_vcpus_internal"], + ), + ] + self.assertEqual(send_event.mock_calls, expected) @unittest.mock.patch("qubes.storage.Storage.create") def test_640_vm_create_disposable(self, mock_storage): From 168a2d4c785cc3b5cb0b4c09a97f335765d45f97 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Tue, 21 Jul 2026 17:24:32 +0200 Subject: [PATCH 05/10] Skip stubdom_xid query if qube is not HVM --- qubes/vm/qubesvm.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index b4db9c44b..9fa9f3ac2 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -1011,6 +1011,9 @@ def stubdom_uuid(self) -> str: @qubes.stateless_property def stubdom_xid(self) -> int: + if self.virt_mode != "hvm": + return -1 + if not self.is_running(): return -1 From 674d130ed882ae00498f77640f9e1882b823fe8e Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Wed, 17 Jun 2026 00:54:49 +0200 Subject: [PATCH 06/10] Hotplug memory for real Changing maxmem property of a running domain allows qmemman to read new information and balance appropriately. For the maxmem property to accurately reflect current state for clients to query, it is forbidden to change it for a running non memory balanced domain. --- qubes/vm/qubesvm.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 9fa9f3ac2..ca6566189 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -2900,6 +2900,33 @@ def create_qdb_entries(self): self.fire_event("domain-qdb-create") + @qubes.events.handler("property-pre-set:maxmem") + def on_property_pre_set_maxmem(self, event, name, newvalue, oldvalue=None): + # pylint: disable=unused-argument + if newvalue == oldvalue: + return + # Necessary for it to reflect the current state for clients to query. + if not self.is_halted() and not self.use_memory_hotplug: + raise qubes.exc.QubesVMNotHaltedError( + "Can't change maxmem of VM {!s}, because it isn't Halted and " + "memory hotplug is forbidden".format(self) + ) + + @qubes.events.handler("property-set:maxmem") + def on_property_set_maxmem(self, event, name, newvalue, oldvalue=None): + # pylint: disable=unused-argument + if not newvalue or newvalue == oldvalue: + return + if not ( + qmemman_present and self.use_memory_hotplug and self.is_running() + ): + return + self.app.vmm.xs.write( + "", + f"/local/domain/{self.xid}/memory/hotplug-max", + str(newvalue * 1024), + ) + # TODO async; update this in constructor def _update_libvirt_domain(self): """Re-initialise :py:attr:`libvirt_domain`.""" From 1ebdd42bfa32264a501406ba79fd182a7fbcf095 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 18 Jun 2026 09:28:44 +0200 Subject: [PATCH 07/10] Report maxmem and fix get_mem variants Add "maxmem" property to "Qubes" (app) and "AdminVM", so clients can now system/host memory and dom0 memory. Fix "get_mem" and "get_mem_static_max" mess: - AdminVM: - "get_mem": supposedly, should report usage in KiB, but it's reading /proc/meminfo and returning kB - "get_mem_static_max": returning total host memory - QubesVM: - "get_mem": supposedly, should report usage, but it's reporting initial memory - "get_mem_static_max": same as above Now all of these methods returns memory in KiB. "get_mem" returns allocated/assigned memory, while "get_mem_static_max" returns maxmem in KiB. --- qubes/app.py | 14 +++++++ qubes/tests/api_admin.py | 3 +- qubes/tests/vm/adminvm.py | 37 ++++++++++++++++- qubes/tests/vm/qubesvm.py | 9 +++++ qubes/vm/adminvm.py | 85 +++++++++++++++++++++++++++------------ qubes/vm/qubesvm.py | 24 +++++------ 6 files changed, 128 insertions(+), 44 deletions(-) diff --git a/qubes/app.py b/qubes/app.py index 87594a178..5570d9e6d 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -1037,6 +1037,11 @@ def get_qube_prop_deps( return system_deps, qube_deps +def _default_maxmem(self) -> int: + """Get maximum memory available to the whole system in KiB.""" + return self.host.memory_total + + class Qubes(qubes.PropertyHolder): """Main Qubes application @@ -1283,6 +1288,15 @@ class Qubes(qubes.PropertyHolder): doc="Check for updates inside qubes", ) + maxmem = qubes.property( + "maxmem", + type=int, + setter=qubes.property.forbidden, + load_stage=3, + default=_default_maxmem, + doc="Maximum system memory in KiB. Unit was chosen for precision.", + ) + def __init__( self, store=None, load=True, offline_mode=None, lock=False, **kwargs ): diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index f30ea9f1b..91d84412d 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -2392,8 +2392,9 @@ def test_411_property_get_all(self): default_shutdown_timeout default=True type=int 60 default_template default=False type=vm test-template management_dispvm default=True type=vm +maxmem default=True type=int {maxmem} stats_interval default=True type=int 3 -updatevm default=True type=vm \n""" +updatevm default=True type=vm \n""".format(maxmem=self.app.maxmem) value = self.call_mgmt_func(b"admin.property.GetAll", b"dom0", b"") self.maxDiff = None self.assertEqual(value, expected) diff --git a/qubes/tests/vm/adminvm.py b/qubes/tests/vm/adminvm.py index ec269cd11..b9ff5d718 100644 --- a/qubes/tests/vm/adminvm.py +++ b/qubes/tests/vm/adminvm.py @@ -22,6 +22,7 @@ import subprocess import unittest import unittest.mock +import libvirt import qubes import qubes.exc @@ -82,7 +83,9 @@ def test_100_xid(self): self.assertEqual(self.vm.xid, 0) def test_101_libvirt_domain(self): + self.assertIsNone(self.vm.libvirt_domain) with unittest.mock.patch.object(self.app, "vmm") as mock_vmm: + mock_vmm.offline_mode = False self.assertIsNotNone(self.vm.libvirt_domain) self.assertEqual( mock_vmm.mock_calls, @@ -91,6 +94,26 @@ def test_101_libvirt_domain(self): ], ) + def test_102_maxmem(self): + # When offline + self.assertEqual(self.vm.maxmem, 4096) + + # Fallback + self.app.vmm = unittest.mock.Mock() + self.app.vmm.offline_mode = False + self.assertEqual(self.vm.maxmem, 4096) + + # From xenstore + val = 1024 * 8 + xenstore = {"/local/domain/0/memory/static-max": str(val).encode()} + self.app.vmm.configure_mock( + **{ + "is_xen.return_value": True, + "xs.read.side_effect": lambda _, path: xenstore[path], + } + ) + self.assertEqual(self.vm.maxmem, val // 1024) + def test_300_is_running(self): self.assertTrue(self.vm.is_running()) @@ -98,11 +121,21 @@ def test_301_get_power_state(self): self.assertEqual(self.vm.get_power_state(), "Running") def test_302_get_mem(self): - self.assertGreater(self.vm.get_mem(), 0) + self.assertEqual(self.vm.get_mem(), 4242) + + self.app.vmm = unittest.mock.Mock() + self.app.vmm.offline_mode = False + self.vm.libvirt_domain.memoryStats = unittest.mock.Mock() + self.vm.libvirt_domain.memoryStats.return_value = {"actual": 4241} + self.assertEqual(self.vm.get_mem(), 4241) + + self.vm.libvirt_domain.memoryStats.side_effect = libvirt.libvirtError + with self.assertRaises(libvirt.libvirtError): + self.vm.get_mem() @unittest.skip("mock object does not support this") def test_303_get_mem_static_max(self): - self.assertGreater(self.vm.get_mem_static_max(), 0) + self.assertEqual(self.vm.get_mem_static_max(), self.vm.maxmem * 1024) def test_310_start(self): with self.assertRaises(qubes.exc.QubesException): diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py index d217a5d46..8798a5d7f 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -243,6 +243,9 @@ def test_010_default_virt_mode(self): self.assertEqual(qubes.vm.qubesvm._default_virt_mode(self.vm), "hvm") def test_020_default_maxmem(self): + self.app.vmm = unittest.mock.Mock() + self.app.vmm.offline_mode.return_value = True + default_maxmem = 2048 self.vm.is_memory_balancing_possible = ( lambda: qubes.vm.qubesvm.QubesVM.is_memory_balancing_possible( @@ -272,6 +275,9 @@ def test_020_default_maxmem(self): ) def test_021_default_maxmem_with_pcidevs(self): + self.app.vmm = unittest.mock.Mock() + self.app.vmm.offline_mode.return_value = True + self.vm.is_memory_balancing_possible = ( lambda: qubes.vm.qubesvm.QubesVM.is_memory_balancing_possible( self.vm @@ -281,6 +287,9 @@ def test_021_default_maxmem_with_pcidevs(self): self.assertEqual(qubes.vm.qubesvm._default_maxmem(self.vm), 0) def test_022_default_maxmem_linux(self): + self.app.vmm = unittest.mock.Mock() + self.app.vmm.offline_mode.return_value = True + self.vm.is_memory_balancing_possible = ( lambda: qubes.vm.qubesvm.QubesVM.is_memory_balancing_possible( self.vm diff --git a/qubes/vm/adminvm.py b/qubes/vm/adminvm.py index 930481c7e..d1409d52c 100644 --- a/qubes/vm/adminvm.py +++ b/qubes/vm/adminvm.py @@ -26,6 +26,8 @@ import grp import subprocess import libvirt +import lxml +import lxml.etree import uuid import qubes @@ -35,6 +37,32 @@ from qubes.vm import LocalVM, BaseVM +def _default_maxmem(self): + if self.app.vmm.offline_mode: + # default value passed on xen cmdline + return 4096 + try: + # Faster to use Xen than to query the fetch the full XML. If one could + # find a libvirt method instead of XMLDesc to get the same value as + # static-max, that would help. + if self.app.vmm.is_xen: + val = self.app.vmm.xs.read("", "/local/domain/0/memory/static-max") + if not isinstance(val, bytes): + raise TypeError("xenstore key static-max is not set") + val = val.decode() + else: + xml = lxml.etree.fromstring(self.libvirt_domain.XMLDesc()) + val = xml.findtext("currentMemory") + if val is None: + raise TypeError("not set") + if not val.isdigit(): + raise ValueError("not a digit") + return int(val) // 1024 + except (TypeError, ValueError, libvirt.libvirtError) as e: + self.log.warning("Failed to get maxmem for dom0: %s", e) + return 4096 + + class AdminVM(LocalVM): """Dom0""" @@ -96,6 +124,14 @@ class AdminVM(LocalVM): doc="Keyboard layout for this VM", ) + maxmem = qubes.property( + "maxmem", + type=int, + setter=qubes.property.forbidden, + default=_default_maxmem, + doc="""Maximum amount of memory available for this VM""", + ) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -146,8 +182,11 @@ def libvirt_domain(self): .. seealso: :py:attr:`qubes.vm.qubesvm.QubesVM.libvirt_domain` """ - if self._libvirt_domain is None: - self._libvirt_domain = self.app.vmm.libvirt_conn.lookupByID(0) + if self._libvirt_domain is not None: + return self._libvirt_domain + if self.app.vmm.offline_mode: + return None + self._libvirt_domain = self.app.vmm.libvirt_conn.lookupByID(0) return self._libvirt_domain @staticmethod @@ -177,22 +216,24 @@ def get_power_state(): """ return "Running" - @staticmethod - def get_mem(): - """Get current memory usage of Dom0. + def get_mem(self): + """Get memory assigned to VM. - Unit is KiB. - - .. seealso: - :py:meth:`qubes.vm.qubesvm.QubesVM.get_mem` + :returns: Memory assigned in KiB. """ - - # return psutil.virtual_memory().total/1024 - with open("/proc/meminfo", encoding="ascii") as file: - for line in file: - if line.startswith("MemTotal:"): - return int(line.split(":")[1].strip().split()[0]) - raise NotImplementedError() + if self.app.vmm.offline_mode: + return 4242 + if self.libvirt_domain is None: + return 0 + try: + if not self.libvirt_domain.isActive(): + return 0 + return self.libvirt_domain.memoryStats()["actual"] + except libvirt.libvirtError as e: + self.log.exception( + "libvirt error code: {!r}".format(e.get_error_code()) + ) + raise def get_mem_static_max(self): """Get maximum memory available to Dom0. @@ -200,14 +241,7 @@ def get_mem_static_max(self): .. seealso: :py:meth:`qubes.vm.qubesvm.QubesVM.get_mem_static_max` """ - if self.app.vmm.offline_mode: - # default value passed on xen cmdline - return 4096 - try: - return self.app.vmm.libvirt_conn.getInfo()[1] - except libvirt.libvirtError as e: - self.log.warning("Failed to get memory limit for dom0: %s", e) - return 4096 + return self.maxmem * 1024 def get_cputime(self): """Get total CPU time burned by Dom0 since start. @@ -363,8 +397,7 @@ async def run_service_for_stdio(self, *args, input=None, **kwargs): return stdouterr def is_memory_balancing_possible(self): - """Check if memory balancing can be enabled. - """ + """Check if memory balancing can be enabled.""" if not self.app.vmm.is_xen: return False return True diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index ca6566189..a193e3e8d 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -2613,20 +2613,17 @@ def on_domain_is_fully_usable(self, event): # memory and disk def get_mem(self): - """Get current memory usage from VM. + """Get memory assigned to VM. - :returns: Memory usage [FIXME unit]. - :rtype: FIXME + :returns: Memory assigned in KiB. + :rtype: int """ - if self.libvirt_domain is None: return 0 - try: if not self.libvirt_domain.isActive(): return 0 - return self.libvirt_domain.info()[1] - + return self.libvirt_domain.memoryStats()["actual"] except libvirt.libvirtError as e: if e.get_error_code() in ( # qube no longer exists @@ -2635,7 +2632,6 @@ def get_mem(self): libvirt.VIR_ERR_INTERNAL_ERROR, ): return 0 - self.log.exception( "libvirt error code: {!r}".format(e.get_error_code()) ) @@ -2644,16 +2640,15 @@ def get_mem(self): def get_mem_static_max(self): """Get maximum memory available to VM. - :returns: Memory limit [FIXME unit]. - :rtype: FIXME + :returns: Memory limit in KiB. + :rtype: int """ - if self.libvirt_domain is None: return 0 - try: - return self.libvirt_domain.maxMemory() - + if not self.libvirt_domain.isActive(): + return 0 + return self.maxmem * 1024 except libvirt.libvirtError as e: if e.get_error_code() in ( # qube no longer exists @@ -2662,7 +2657,6 @@ def get_mem_static_max(self): libvirt.VIR_ERR_INTERNAL_ERROR, ): return 0 - self.log.exception( "libvirt error code: {!r}".format(e.get_error_code()) ) From ab37da74c19fa7f8f7f6c43de3d0a191d295b3e6 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Mon, 6 Jul 2026 10:14:12 +0200 Subject: [PATCH 08/10] Report more CPU information to QubesHost --- qubes/app.py | 84 ++++++++++++++++++++++++------------- qubes/tests/api_admin.py | 1 + qubes/tests/app.py | 91 ++++++++++++++++++++-------------------- 3 files changed, 101 insertions(+), 75 deletions(-) diff --git a/qubes/app.py b/qubes/app.py index 5570d9e6d..5ae0be412 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -287,22 +287,41 @@ class QubesHost: def __init__(self, app): self.app = app - self._no_cpus = None + self._total_mem = None + self._no_cpus = None + self._physinfo = None + self._cpu_arch = None self._cpu_family = None - self._cpu_model = None def _fetch(self): if self._no_cpus is not None: return - # pylint: disable=unused-variable - model, memory, cpus, mhz, nodes, socket, cores, threads = ( - self.app.vmm.libvirt_conn.getInfo() - ) - self._total_mem = int(memory) * 1024 - self._no_cpus = cpus + # ['x86_64', 32404, 16, 2995, 1, 1, 16, 1] + ( + cpu_arch, + mem_mebibyte, + no_cpus, + _mhz, + _nodes, + _socket, + _cores, + _threads, + ) = self.app.vmm.libvirt_conn.getInfo() + + self._cpu_arch = cpu_arch + self._no_cpus = no_cpus + + if self.app.vmm.is_xen: + self._physinfo = self.app.vmm.xc.physinfo() + + # Avoid rounding differences. + if self._physinfo: + self._total_mem = int(self._physinfo["total_memory"]) + else: + self._total_mem = int(mem_mebibyte) * 1024 self.app.log.debug( "QubesHost: no_cpus={} memory_total={}".format( @@ -317,28 +336,25 @@ def _fetch(self): ) @property - def memory_total(self): - """Total memory, in kbytes""" - + def no_cpus(self) -> int: + """Number of CPUs""" if self.app.vmm.offline_mode: - return 2**64 - 1 + return 42 self._fetch() - return self._total_mem + return self._no_cpus @property - def no_cpus(self): - """Number of CPUs""" - + def memory_total(self) -> int: + """Total memory, in kbytes""" if self.app.vmm.offline_mode: - return 42 - + return 2**64 - 1 self._fetch() - return self._no_cpus + return self._total_mem @property def cpu_family_model(self): """Get CPU family and model""" - if self._cpu_family is None or self._cpu_model is None: + if self._cpu_family is None or self._cpu_arch is None: family = None model = None with open("/proc/cpuinfo", encoding="ascii") as cpuinfo: @@ -353,8 +369,8 @@ def cpu_family_model(self): elif field.strip() == "cpu family": family = int(value.strip()) self._cpu_family = family - self._cpu_model = model - return self._cpu_family, self._cpu_model + self._cpu_arch = model + return self._cpu_family, self._cpu_arch def get_free_xen_memory(self): """Get free memory from Xen's physinfo. @@ -363,17 +379,14 @@ def get_free_xen_memory(self): """ if not self.app.vmm.is_xen: raise NotImplementedError("This function requires Xen hypervisor") - self._physinfo = self.app.vmm.xc.physinfo() + self._fetch() return int(self._physinfo["free_memory"]) def is_iommu_supported(self): """Check if IOMMU is supported on this platform""" - if self._physinfo is None: - if not self.app.vmm.is_xen: - raise NotImplementedError( - "This function requires Xen hypervisor" - ) - self._physinfo = self.app.vmm.xc.physinfo() + if not self.app.vmm.is_xen: + raise NotImplementedError("This function requires Xen hypervisor") + self._fetch() return "hvm_directio" in self._physinfo["virt_caps"] def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): @@ -1042,6 +1055,10 @@ def _default_maxmem(self) -> int: return self.host.memory_total +def _default_no_cpus(self) -> int: + return self.host.no_cpus + + class Qubes(qubes.PropertyHolder): """Main Qubes application @@ -1297,6 +1314,15 @@ class Qubes(qubes.PropertyHolder): doc="Maximum system memory in KiB. Unit was chosen for precision.", ) + no_cpus = qubes.property( + "no_cpus", + type=int, + setter=qubes.property.forbidden, + load_stage=3, + default=_default_no_cpus, + doc="Quantity of CPUs", + ) + def __init__( self, store=None, load=True, offline_mode=None, lock=False, **kwargs ): diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 91d84412d..7423b2af8 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -2393,6 +2393,7 @@ def test_411_property_get_all(self): default_template default=False type=vm test-template management_dispvm default=True type=vm maxmem default=True type=int {maxmem} +no_cpus default=True type=int 42 stats_interval default=True type=int 3 updatevm default=True type=vm \n""".format(maxmem=self.app.maxmem) value = self.call_mgmt_func(b"admin.property.GetAll", b"dom0", b"") diff --git a/qubes/tests/app.py b/qubes/tests/app.py index de41891a7..d455869cc 100644 --- a/qubes/tests/app.py +++ b/qubes/tests/app.py @@ -39,7 +39,10 @@ class TestApp(qubes.tests.TestEmitter): - pass + + def __init__(self): + super().__init__() + self.log = mock.Mock() class TC_20_QubesHost(qubes.tests.QubesTestCase): @@ -50,63 +53,59 @@ def setUp(self): self.qubes_host = qubes.app.QubesHost(self.app) self.maxDiff = None - def test_010_iommu_supported(self): + self.libvirt_info = ["x86_64", 32404, 16, 2995, 1, 1, 16, 1] + self.xen_physinfo = { + "nr_nodes": 1, + "threads_per_core": 1, + "cores_per_socket": 16, + "nr_cpus": 16, + "total_memory": 33182508, + "free_memory": 23905768, + "scrub_memory": 0, + "cpu_khz": 2995195, + "hw_caps": "...", + "virt_caps": "hvm pv hvm_directio pv_directio", + } self.app.vmm.configure_mock( **{ - "xc.physinfo.return_value": { - "hw_caps": "...", - "scrub_memory": 0, - "virt_caps": "hvm hvm_directio", - "nr_cpus": 4, - "threads_per_core": 1, - "cpu_khz": 3400001, - "nr_nodes": 1, - "free_memory": 234752, - "cores_per_socket": 4, - "total_memory": 16609720, - } + "libvirt_conn.getInfo.return_value": self.libvirt_info, + "xc.physinfo.return_value": self.xen_physinfo, } ) + + def test_010_iommu_supported(self): + physinfo = self.xen_physinfo + physinfo["virt_caps"] = "hvm hvm_directio" + self.app.vmm.configure_mock(**{"xc.physinfo.return_value": physinfo}) self.assertEqual(self.qubes_host.is_iommu_supported(), True) def test_011_iommu_supported(self): - self.app.vmm.configure_mock( - **{ - "xc.physinfo.return_value": { - "hw_caps": "...", - "scrub_memory": 0, - "virt_caps": "hvm hvm_directio pv pv_directio", - "nr_cpus": 4, - "threads_per_core": 1, - "cpu_khz": 3400001, - "nr_nodes": 1, - "free_memory": 234752, - "cores_per_socket": 4, - "total_memory": 16609720, - } - } - ) + physinfo = self.xen_physinfo + physinfo["virt_caps"] = "hvm hvm_directio pv pv_directio" + self.app.vmm.configure_mock(**{"xc.physinfo.return_value": physinfo}) self.assertEqual(self.qubes_host.is_iommu_supported(), True) def test_012_iommu_supported(self): - self.app.vmm.configure_mock( - **{ - "xc.physinfo.return_value": { - "hw_caps": "...", - "scrub_memory": 0, - "virt_caps": "hvm pv", - "nr_cpus": 4, - "threads_per_core": 1, - "cpu_khz": 3400001, - "nr_nodes": 1, - "free_memory": 234752, - "cores_per_socket": 4, - "total_memory": 16609720, - } - } - ) + physinfo = self.xen_physinfo + physinfo["virt_caps"] = "hvm pv" + self.app.vmm.configure_mock(**{"xc.physinfo.return_value": physinfo}) self.assertEqual(self.qubes_host.is_iommu_supported(), False) + def test_030_attrs(self): + self.app.vmm.offline_mode = False + self.qubes_host = qubes.app.QubesHost(self.app) + self.assertEqual( + self.qubes_host.memory_total, self.xen_physinfo["total_memory"] + ) + self.assertEqual(self.qubes_host.no_cpus, self.libvirt_info[2]) + + self.app.vmm.is_xen = False + self.app.vmm.offline_mode = False + self.qubes_host = qubes.app.QubesHost(self.app) + self.assertEqual( + self.qubes_host.memory_total, self.libvirt_info[1] * 1024 + ) + class TC_30_VMCollection(qubes.tests.QubesTestCase): def setUp(self): From 630d8511b589435b0db97fd7ce473c6d524b85aa Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Fri, 17 Jul 2026 17:01:26 +0200 Subject: [PATCH 09/10] Deprecate cpu_usage_raw from Stats event Breaking change. No known Qubes OS code uses. A better value is "cpu_usage". --- qubes/api/admin.py | 1 - qubes/app.py | 4 ---- qubes/tests/api_admin.py | 12 ------------ 3 files changed, 17 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 8a18c2b12..4d3d890db 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -2524,7 +2524,6 @@ def _send_stats_single(self, info_time, info, only_vm, filters): "memory_with_swap_used": int(vm_info["memory_with_swap_used"]), "cpu_time": int(vm_info["cpu_time"] / 1000000), "cpu_usage": int(vm_info["cpu_usage"]), - "cpu_usage_raw": int(vm_info["cpu_usage_raw"]), "online_vcpus": int(vm_info["online_vcpus"]), } diff --git a/qubes/app.py b/qubes/app.py index 5ae0be412..45cc29e7b 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -470,10 +470,6 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): - Description: Amount memory assigned that is usable in the qube. Will be deprecated in a future release due to its ambiguous name. Prefer the equivalent ``memory_assigned_usable``. - - ``cpu_usage_raw``: - - Type: ``int``, percentage. - - Description: CPU usage. Will be deprecated as it can be calculated - on the client. This function requires Xen hypervisor for detailed overview. diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 7423b2af8..19db93291 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -4088,7 +4088,6 @@ def test_630_vm_stats(self): "swap_used": 0, "cpu_time": 243951379111104 // 8, "cpu_usage": 0, - "cpu_usage_raw": 0, "online_vcpus": 16, }, 1: { @@ -4101,16 +4100,13 @@ def test_630_vm_stats(self): "swap_used": 0, "cpu_time": 2849496569205, "cpu_usage": 0, - "cpu_usage_raw": 0, "online_vcpus": 2, }, } stats2 = copy.deepcopy(stats1) stats2[0]["cpu_time"] += 100000000 stats2[0]["cpu_usage"] = 10 - stats2[0]["cpu_usage_raw"] = 10 stats2[1]["cpu_usage"] = 5 - stats2[1]["cpu_usage_raw"] = 5 self.app.host.get_vm_stats = unittest.mock.Mock() self.app.host.get_vm_stats.side_effect = [ (0, stats1), @@ -4171,7 +4167,6 @@ def name(self): swap_used=stats1[0]["swap_used"], cpu_time=stats1[0]["cpu_time"] // 1000000, cpu_usage=stats1[0]["cpu_usage"], - cpu_usage_raw=stats1[0]["cpu_usage_raw"], online_vcpus=stats1[0]["online_vcpus"], ), unittest.mock.call( @@ -4184,7 +4179,6 @@ def name(self): swap_used=stats1[1]["swap_used"], cpu_time=stats1[1]["cpu_time"] // 1000000, cpu_usage=stats1[1]["cpu_usage"], - cpu_usage_raw=stats1[1]["cpu_usage_raw"], online_vcpus=stats1[1]["online_vcpus"], ), unittest.mock.call( @@ -4197,7 +4191,6 @@ def name(self): swap_used=stats2[0]["swap_used"], cpu_time=stats2[0]["cpu_time"] // 1000000, cpu_usage=stats2[0]["cpu_usage"], - cpu_usage_raw=stats2[0]["cpu_usage_raw"], online_vcpus=stats2[0]["online_vcpus"], ), unittest.mock.call( @@ -4210,7 +4203,6 @@ def name(self): swap_used=stats2[1]["swap_used"], cpu_time=stats2[1]["cpu_time"] // 1000000, cpu_usage=stats2[1]["cpu_usage"], - cpu_usage_raw=stats2[1]["cpu_usage_raw"], online_vcpus=stats2[1]["online_vcpus"], ), ] @@ -4231,7 +4223,6 @@ def test_631_vm_stats_single_vm(self): "swap_used": 0, "cpu_time": 2849496569205, "cpu_usage": 0, - "cpu_usage_raw": 0, "online_vcpus": 2, "cpu_time_internal": 2849496569205, "cpu_usage_internal": 0.0, @@ -4240,7 +4231,6 @@ def test_631_vm_stats_single_vm(self): } stats2 = copy.deepcopy(stats1) stats2[2]["cpu_usage"] = 5 - stats2[2]["cpu_usage_raw"] = 5 self.app.host.get_vm_stats = unittest.mock.Mock() self.app.host.get_vm_stats.side_effect = [ (0, stats1), @@ -4300,7 +4290,6 @@ def name(self): swap_used=stats1[2]["swap_used"], cpu_time=stats1[2]["cpu_time"] // 1000000, cpu_usage=stats1[2]["cpu_usage"], - cpu_usage_raw=stats1[2]["cpu_usage_raw"], online_vcpus=stats1[2]["online_vcpus"], cpu_time_internal=stats1[2]["cpu_time_internal"] // 1000000, cpu_usage_internal=stats1[2]["cpu_usage_internal"], @@ -4316,7 +4305,6 @@ def name(self): swap_used=stats2[2]["swap_used"], cpu_time=stats2[2]["cpu_time"] // 1000000, cpu_usage=stats2[2]["cpu_usage"], - cpu_usage_raw=stats2[2]["cpu_usage_raw"], online_vcpus=stats2[2]["online_vcpus"], cpu_time_internal=stats2[2]["cpu_time_internal"] // 1000000, cpu_usage_internal=stats2[2]["cpu_usage_internal"], From 0224b77551c745151e99c0e8ddda30d104770d1f Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Wed, 22 Jul 2026 16:30:58 +0200 Subject: [PATCH 10/10] Avoid reads to xenstore keys that are not written Xenstore keys are created immediately after domain is crated and before tasks can be switched with "await", to guarantee that qubesd methods (same thread) don't fail attempts to read xenstore keys. This is the case for "Qubes.get_vm_stats", which interacts with VMM methods instead of qubes events. --- qubes/app.py | 56 ++++++++++++++++++++++++++------------------- qubes/tests/app.py | 16 ++++++++++--- qubes/vm/qubesvm.py | 34 ++++++++++++++------------- 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/qubes/app.py b/qubes/app.py index 45cc29e7b..a7131c2ff 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -584,6 +584,27 @@ def query_stubdom_xid(domid) -> int: return int(stubdom_xid_str) return -1 + def query_xs_memory(domid, key) -> int | None: + if ( + f"xs_{key}" in current[domid] + and current[domid][f"xs_{key}"] is False + ): + return None + untrusted_value = self.app.vmm.xs.read( + "", f"/local/domain/{domid}/memory/{key}" + ) + if untrusted_value is None: + current[domid][f"xs_{key}"] = False + del untrusted_value + return None + current[domid][f"xs_{key}"] = True + if not untrusted_value.isdigit(): + del untrusted_value + return None + value = int(untrusted_value) + del untrusted_value + return value + info = sorted(info, key=lambda domain: domain["domid"]) info = {v["domid"]: v for v in info} for domid in info.keys(): @@ -598,6 +619,12 @@ def query_stubdom_xid(domid) -> int: current[domid]["name"] = previous[domid]["name"] current[domid]["is_stubdom"] = previous[domid]["is_stubdom"] current[domid]["stubdom_xid"] = previous[domid]["stubdom_xid"] + current[domid]["xs_meminfo"] = previous[domid].get( + "xs_meminfo", None + ) + current[domid]["xs_swapinfo"] = previous[domid].get( + "xs_swapinfo", None + ) stubdom_xid = current[domid]["stubdom_xid"] elif "name" not in current[domid]: name = self.app.get_name_from_domid(domid) @@ -637,32 +664,13 @@ def query_stubdom_xid(domid) -> int: ] if self.app.vmm.is_xen and not current[domid]["is_stubdom"]: - untrusted_meminfo = self.app.vmm.xs.read( - "", f"/local/domain/{domid}/memory/meminfo" - ) - if ( - untrusted_meminfo is not None - and untrusted_meminfo.isdigit() - ): - meminfo = int(untrusted_meminfo) - del untrusted_meminfo + + meminfo = query_xs_memory(domid, "meminfo") + if meminfo is not None: current[domid]["memory_with_swap_used"] = meminfo - # Only query swapinfo is meminfo is valid to avoid - # extraneous calls. - untrusted_swapinfo = self.app.vmm.xs.read( - "", f"/local/domain/{domid}/memory/swapinfo" - ) - if ( - untrusted_swapinfo is not None - and untrusted_swapinfo.isdigit() - ): - swapinfo = int(untrusted_swapinfo) - del untrusted_swapinfo + swapinfo = query_xs_memory(domid, "swapinfo") + if swapinfo is not None: current[domid]["swap_used"] = swapinfo - else: - del untrusted_swapinfo - else: - del untrusted_meminfo current[domid]["online_vcpus"] = info[domid]["online_vcpus"] if stubdom_xid > 0: diff --git a/qubes/tests/app.py b/qubes/tests/app.py index d455869cc..66b682a2d 100644 --- a/qubes/tests/app.py +++ b/qubes/tests/app.py @@ -879,6 +879,8 @@ def test_000_get_vm_stats_single(self, mock_qubesdb, mock_adminvm_qubesdb): "cpu_usage_raw": 0, "online_vcpus": 8, "stubdom_xid": -1, + "xs_meminfo": True, + "xs_swapinfo": False, }, 1: { "name": names[1], @@ -893,6 +895,8 @@ def test_000_get_vm_stats_single(self, mock_qubesdb, mock_adminvm_qubesdb): "cpu_usage_raw": 0, "online_vcpus": 1, "stubdom_xid": -1, + "xs_meminfo": True, + "xs_swapinfo": True, }, 2: { "name": names[2], @@ -907,6 +911,8 @@ def test_000_get_vm_stats_single(self, mock_qubesdb, mock_adminvm_qubesdb): "cpu_usage_raw": 0, "online_vcpus": 8, "stubdom_xid": -1, + "xs_meminfo": True, + "xs_swapinfo": True, }, } self.assertEqual(info, expected_info) @@ -925,7 +931,7 @@ def test_001_get_vm_stats_twice(self, mock_qubesdb, mock_adminvm_qubesdb): "/local/domain/1/memory/meminfo": b"849925", "/local/domain/2/memory/meminfo": b"849926", "/local/domain/0/memory/swapinfo": b"0", - "/local/domain/1/memory/swapinfo": b"35", + "/local/domain/1/memory/swapinfo": None, "/local/domain/2/memory/swapinfo": b"4", } self.app.get_name_from_domid = lambda domid: names[domid] @@ -970,6 +976,8 @@ def test_001_get_vm_stats_twice(self, mock_qubesdb, mock_adminvm_qubesdb): "cpu_usage_raw": 80, "online_vcpus": 8, "stubdom_xid": -1, + "xs_meminfo": True, + "xs_swapinfo": True, }, 1: { "name": names[1], @@ -978,12 +986,13 @@ def test_001_get_vm_stats_twice(self, mock_qubesdb, mock_adminvm_qubesdb): "memory_assigned_usable": 303916, "memory_kb": 303916, "memory_with_swap_used": 849925, - "swap_used": 35, "cpu_time": 2849496569205, "cpu_usage": 100, "cpu_usage_raw": 100, "online_vcpus": 1, "stubdom_xid": -1, + "xs_meminfo": True, + "xs_swapinfo": False, }, 2: { "name": names[2], @@ -998,6 +1007,8 @@ def test_001_get_vm_stats_twice(self, mock_qubesdb, mock_adminvm_qubesdb): "cpu_usage_raw": 100, "online_vcpus": 8, "stubdom_xid": -1, + "xs_meminfo": True, + "xs_swapinfo": True, }, } self.assertEqual(info, expected_info) @@ -1016,7 +1027,6 @@ def test_001_get_vm_stats_twice(self, mock_qubesdb, mock_adminvm_qubesdb): ("xs.read", ("", "/local/domain/0/memory/meminfo")), ("xs.read", ("", "/local/domain/0/memory/swapinfo")), ("xs.read", ("", "/local/domain/1/memory/meminfo")), - ("xs.read", ("", "/local/domain/1/memory/swapinfo")), ("xs.read", ("", "/local/domain/2/memory/meminfo")), ("xs.read", ("", "/local/domain/2/memory/swapinfo")), ], diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index a193e3e8d..b9817ec8e 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -1496,6 +1496,7 @@ async def start( self.libvirt_domain.createWithFlags( libvirt.VIR_DOMAIN_START_PAUSED ) + self.create_xs_entries() # the above allocates xid, lets announce that self.fire_event("property-reset:xid", name="xid") @@ -2811,6 +2812,23 @@ def relative_path(self, path): return os.path.relpath(path, self.dir_path) + def create_xs_entries(self): + # TODO: Currently the whole qmemman is quite Xen-specific, so stay with + # xenstore for it until decided otherwise + if qmemman_present and self.maxmem: + xs_basedir = f"/local/domain/{self.xid}" + for key in ["meminfo", "swapinfo"]: + self.app.vmm.xs.write("", f"{xs_basedir}/memory/{key}", "") + self.app.vmm.xs.set_permissions( + "", f"{xs_basedir}/memory/{key}", [{"dom": self.xid}] + ) + if self.use_memory_hotplug: + self.app.vmm.xs.write( + "", + f"{xs_basedir}/memory/hotplug-max", + str(self.maxmem * 1024), + ) + def create_qdb_entries(self): """Create entries in Qubes DB.""" # pylint: disable=no-member @@ -2876,22 +2894,6 @@ def create_qdb_entries(self): self.untrusted_qdb.write("/qubes-block-devices", "") self.untrusted_qdb.write("/qubes-usb-devices", "") - # TODO: Currently the whole qmemman is quite Xen-specific, so stay with - # xenstore for it until decided otherwise - if qmemman_present and self.maxmem: - xs_basedir = f"/local/domain/{self.xid}" - for key in ["meminfo", "swapinfo"]: - self.app.vmm.xs.write("", f"{xs_basedir}/memory/{key}", "") - self.app.vmm.xs.set_permissions( - "", f"{xs_basedir}/memory/{key}", [{"dom": self.xid}] - ) - if self.use_memory_hotplug: - self.app.vmm.xs.write( - "", - f"{xs_basedir}/memory/hotplug-max", - str(self.maxmem * 1024), - ) - self.fire_event("domain-qdb-create") @qubes.events.handler("property-pre-set:maxmem")