diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 6a7f0c176..4d3d890db 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,35 +2508,39 @@ 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 - self.send_event( - name, - "vm-stats", - memory_kb=int(vm_info["memory_kb"]), - cpu_time=int(vm_info["cpu_time"] / 1000000), - cpu_usage=int(vm_info["cpu_usage"]), - cpu_usage_raw=int(vm_info["cpu_usage_raw"]), - ) + 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_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"]), + "online_vcpus": int(vm_info["online_vcpus"]), + } + + 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) return info_time, info @@ -2566,11 +2567,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 d9699654f..a7131c2ff 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): @@ -384,17 +397,86 @@ 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: - - 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 - This function requires Xen hypervisor. + - ``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, 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``: + - Type: ``int``, unit: ``KiB``. + - 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. 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. + - ``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. + + 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``: + - 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``. + + 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 @@ -402,6 +484,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( @@ -413,39 +496,225 @@ 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) - # TODO: add stubdomain stats to actual VMs - for vm in info: - domid = vm["domid"] + 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( + "", + 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 + + 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(): + stubdom_xid = -1 current[domid] = {} - current[domid]["memory_kb"] = vm["mem_kb"] - current[domid]["cpu_time"] = round(vm["cpu_time"]) - vcpus = max(vm["online_vcpus"], 1) + + for key in ["name", "is_stubdom"]: + if key in info[domid]: + current[domid][key] = info[domid][key] + 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]["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 ) - if current[domid]["cpu_usage_raw"] < 0: - # VM has been rebooted - current[domid]["cpu_usage_raw"] = 0 + 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) + 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 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] + + if current[domid]["is_stubdom"]: + current[domid]["cpu_time"] = info[domid]["cpu_time"] + continue + + 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" + ] + + if self.app.vmm.is_xen and not current[domid]["is_stubdom"]: + + meminfo = query_xs_memory(domid, "meminfo") + if meminfo is not None: + current[domid]["memory_with_swap_used"] = meminfo + swapinfo = query_xs_memory(domid, "swapinfo") + if swapinfo is not None: + current[domid]["swap_used"] = swapinfo + + 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"] = calculate_cpu_usage_raw( + current[domid]["cpu_time"], previous[domid]["cpu_time"] + ) + 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"] / 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 @@ -785,6 +1054,15 @@ 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 + + +def _default_no_cpus(self) -> int: + return self.host.no_cpus + + class Qubes(qubes.PropertyHolder): """Main Qubes application @@ -1031,6 +1309,24 @@ 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.", + ) + + 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 ): @@ -1886,3 +2182,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 cdf2f96e8..19db93291 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -2392,8 +2392,10 @@ 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} +no_cpus default=True type=int 42 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) @@ -4077,24 +4079,34 @@ 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, + "swap_used": 0, "cpu_time": 243951379111104 // 8, "cpu_usage": 0, - "cpu_usage_raw": 0, - "memory_kb": 3733212, + "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, + "swap_used": 0, "cpu_time": 2849496569205, "cpu_usage": 0, - "cpu_usage_raw": 0, - "memory_kb": 303916, + "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), @@ -4142,59 +4154,83 @@ 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", - 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"], - ), - unittest.mock.call( - "test-template", - "vm-stats", - 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"], - ), - unittest.mock.call( - "dom0", - "vm-stats", - 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"], - ), - unittest.mock.call( - "test-template", - "vm-stats", - 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"], - ), - ], - ) + + 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"], + swap_used=stats1[0]["swap_used"], + cpu_time=stats1[0]["cpu_time"] // 1000000, + cpu_usage=stats1[0]["cpu_usage"], + 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"], + swap_used=stats1[1]["swap_used"], + cpu_time=stats1[1]["cpu_time"] // 1000000, + cpu_usage=stats1[1]["cpu_usage"], + 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"], + swap_used=stats2[0]["swap_used"], + cpu_time=stats2[0]["cpu_time"] // 1000000, + cpu_usage=stats2[0]["cpu_usage"], + 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"], + swap_used=stats2[1]["swap_used"], + cpu_time=stats2[1]["cpu_time"] // 1000000, + cpu_usage=stats2[1]["cpu_usage"], + 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, + "swap_used": 0, "cpu_time": 2849496569205, "cpu_usage": 0, - "cpu_usage_raw": 0, - "memory_kb": 303916, + "online_vcpus": 2, + "cpu_time_internal": 2849496569205, + "cpu_usage_internal": 0.0, + "online_vcpus_internal": 1, }, } 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), @@ -4242,28 +4278,40 @@ 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", - 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"], - ), - unittest.mock.call( - "test-vm1", - "vm-stats", - 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"], - ), - ], - ) + 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"], + 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"], + 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): diff --git a/qubes/tests/app.py b/qubes/tests/app.py index 55a6e5091..66b682a2d 100644 --- a/qubes/tests/app.py +++ b/qubes/tests/app.py @@ -39,104 +39,13 @@ class TestApp(qubes.tests.TestEmitter): - pass + def __init__(self): + super().__init__() + self.log = mock.Mock() -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, - }, - ] +class TC_20_QubesHost(qubes.tests.QubesTestCase): def setUp(self): super().setUp() self.app = TestApp() @@ -144,161 +53,59 @@ 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: { - "cpu_time": 243951379111104, - "cpu_usage": 0, - "cpu_usage_raw": 0, - "memory_kb": 3733212, - }, - 1: { - "cpu_time": 2849496569205, - "cpu_usage": 0, - "cpu_usage_raw": 0, - "memory_kb": 303916, - }, - 11: { - "cpu_time": 249658663079978, - "cpu_usage": 0, - "cpu_usage_raw": 0, - "memory_kb": 3782668, - }, - } - 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: { - "cpu_time": 243951379111104, - "cpu_usage": 10, - "cpu_usage_raw": 80, - "memory_kb": 3733212, - }, - 1: { - "cpu_time": 2849496569205, - "cpu_usage": 100, - "cpu_usage_raw": 100, - "memory_kb": 303916, - }, - 11: { - "cpu_time": 249658663079978, - "cpu_usage": 12, - "cpu_usage_raw": 100, - "memory_kb": 3782668, - }, + 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.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] - ] + "libvirt_conn.getInfo.return_value": self.libvirt_info, + "xc.physinfo.return_value": self.xen_physinfo, } ) - 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( - **{ - "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, - } - } - ) + 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): @@ -863,6 +670,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( @@ -896,6 +797,7 @@ def setUp(self): qube.template_for_dispvms = True self.app.save() self.emitter = qubes.tests.TestEmitter() + self.maxDiff = None def tearDown(self): try: @@ -915,6 +817,251 @@ 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", + "/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"} + 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/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) + 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, + "xs_meminfo": True, + "xs_swapinfo": False, + }, + 1: { + "name": names[1], + "is_stubdom": False, + "memory_assigned_total": 308224, + "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, + "online_vcpus": 1, + "stubdom_xid": -1, + "xs_meminfo": True, + "xs_swapinfo": True, + }, + 2: { + "name": names[2], + "is_stubdom": False, + "memory_assigned_total": 3783692, + "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, + "online_vcpus": 8, + "stubdom_xid": -1, + "xs_meminfo": True, + "xs_swapinfo": True, + }, + } + 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", + "/local/domain/0/memory/swapinfo": b"0", + "/local/domain/1/memory/swapinfo": None, + "/local/domain/2/memory/swapinfo": b"4", + } + 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, + "swap_used": 0, + "cpu_time": 243951379111104, + "cpu_usage": 10, + "cpu_usage_raw": 80, + "online_vcpus": 8, + "stubdom_xid": -1, + "xs_meminfo": True, + "xs_swapinfo": True, + }, + 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, + "xs_meminfo": True, + "xs_swapinfo": False, + }, + 2: { + "name": names[2], + "is_stubdom": False, + "memory_assigned_total": 3783692, + "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, + "online_vcpus": 8, + "stubdom_xid": -1, + "xs_meminfo": True, + "xs_swapinfo": True, + }, + } + 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/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/2/memory/meminfo")), + ("xs.read", ("", "/local/domain/2/memory/swapinfo")), + ], + ) + + 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/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 80acbbbfb..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. - - Unit is KiB. + def get_mem(self): + """Get memory assigned to VM. - .. 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. @@ -362,6 +396,12 @@ 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..b9817ec8e 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 @@ -1093,11 +1096,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 @@ -1490,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") @@ -2011,6 +2018,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 +2088,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) @@ -2603,20 +2614,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 @@ -2625,7 +2633,6 @@ def get_mem(self): libvirt.VIR_ERR_INTERNAL_ERROR, ): return 0 - self.log.exception( "libvirt error code: {!r}".format(e.get_error_code()) ) @@ -2634,16 +2641,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 @@ -2652,7 +2658,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()) ) @@ -2807,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 @@ -2872,22 +2894,34 @@ 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}" - self.app.vmm.xs.write("", f"{xs_basedir}/memory/meminfo", "") - self.app.vmm.xs.set_permissions( - "", f"{xs_basedir}/memory/meminfo", [{"dom": self.xid}] + 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) ) - 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-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):