Skip to content
114 changes: 78 additions & 36 deletions qubes/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import re
import string
import subprocess
import time

from ctypes import CDLL, c_bool

Expand Down Expand Up @@ -2494,54 +2495,59 @@ 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, 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)
"""

info_time, info = self.app.host.get_vm_stats(
info_time, info, only_vm=only_vm
_info_time, info = self.app.host.get_vm_stats(
previous=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
for vm_info in info.values():
name = vm_info["name"]
if name is None:
continue

if only_vm and only_vm.name != name:
continue
if vm_info["is_stubdom"]:
continue
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
return info

@qubes.api.method(
"admin.vm.Stats",
Expand All @@ -2564,15 +2570,51 @@ async def vm_stats(self):

self.send_event(self.app, "connection-established")

info_time = None
# TODO: ben:
# marek said: https://github.com/QubesOS/qubes-core-admin/pull/827#discussion_r3690517835
#
# > The change to use `self.stats_cache_time` instead of the explicit
# > value from the parameter is not great. If nobody is updating
# > (requesting) stats for some time, next one will return average of
# > a long time pretending it's the "current" value.
#
# A long average is not good but not that bad, considering we
# currently only send 0. That value is also only seen on the first
# request after the cache was used after a long time, and for as long
# as the first interval. Anyway, I can discard time based values from
# previous, if the cache is very outdated, for example, using "if
# cache_diff >= self.app.stats_interval * 2", discard the cache, force
# refresh.
#
# Another benefit of this caching, if we want almost "live" data, we
# can reduce the default "stats_interval" to 1.
#
# > The way you "deprecated" the parameter is also not great - if
# > there is some use remaining, it will spam the log every 5s or so.
#
# This was fixed, it is only sent if there is no cache, so once per
# qubesd start + admin.vm.Stats call.

info = None
id_to_name_map = {0: "dom0"}
interval = self.app.stats_interval
if self.app.host.stats_cache_time:
current_time = time.time()
cache_diff = current_time - self.app.host.stats_cache_time
# Adjusting the first sleep allows the client to query from cache
# on first request, sleep just enough for the cache to have been
# generated and on second request, be the first to generate the
# cache or use the cache. From then on, it will sleep that
# ``stats_interval``.
interval = self.app.stats_interval - cache_diff
if interval <= 0:
interval = self.app.stats_interval
try:
while True:
info_time, info = self._send_stats_single(
info_time, info, only_vm, stats_filters, id_to_name_map
info = self._send_stats_single(
info=info, only_vm=only_vm, filters=stats_filters
)
await asyncio.sleep(self.app.stats_interval)
await asyncio.sleep(interval)
interval = self.app.stats_interval
except asyncio.CancelledError:
# valid method to terminate this loop
pass
Expand Down
Loading