diff --git a/vrtManager/instance.py b/vrtManager/instance.py index b3bbd066..278bcf87 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -15,6 +15,9 @@ try: from libvirt import ( VIR_DOMAIN_AFFECT_CONFIG, VIR_DOMAIN_AFFECT_LIVE, + VIR_DOMAIN_STATS_BLOCK, + VIR_DOMAIN_STATS_CPU_TOTAL, + VIR_DOMAIN_STATS_INTERFACE, VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_ARP, VIR_DOMAIN_PAUSED, VIR_DOMAIN_RUNNING, @@ -92,6 +95,7 @@ HOST_DEVICES = "devices/hostdev | devices/interface[@type='hostdev'] | devices/i SNAPSHOT_NAME = re.compile(r"[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}") # the longest window wvmInstance.usage() averages over USAGE_MAX_WINDOW = 30 +USAGE_STATS = VIR_DOMAIN_STATS_CPU_TOTAL | VIR_DOMAIN_STATS_BLOCK | VIR_DOMAIN_STATS_INTERFACE # seconds the guest agent has to set a password or add a key AGENT_TIMEOUT = 10 # A running VM is cloned with a push backup that keeps the VM alive when the @@ -1434,15 +1438,24 @@ class wvmInstance(wvmConnect): } key = [self.instance.ID(), self.host_info[2], disks, nics] - unread = (-1,) * 8 + unread = [-1, -1] def read(): - blocks = [self.instance.blockStats(dev) for dev in disks] - links = [self.instance.interfaceStats(target) if target else unread for _, target in nics] + # one call for the CPU time and every disk's and NIC's counters + [(_dom, stats)] = self.wvm.domainListGetStats([self.instance], USAGE_STATS) + + def counters(kind, names): + """{device name: [its counters]} of stats' kind (block, net); -1 for one not reported.""" + return {stats.get(f"{kind}.{i}.name"): [stats.get(f"{kind}.{i}.{name}", -1) for name in names] + for i in range(stats.get(f"{kind}.count", 0))} + + blocks = counters("block", ("rd.bytes", "wr.bytes")) + links = counters("net", ("rx.bytes", "tx.bytes")) return [ - self.instance.info()[4], - [[stats[1], stats[3]] for stats in blocks], - [[stats[0], stats[4]] for stats in links], + # a driver without these stats (libvirt's test driver) still has info() + stats["cpu.time"] if "cpu.time" in stats else self.instance.info()[4], + [blocks.get(dev, unread) for dev in disks], + [links.get(target, unread) if target else unread for _, target in nics], time.monotonic(), ] diff --git a/vrtManager/tests/test_usage.py b/vrtManager/tests/test_usage.py index 0ef44192..7516ed60 100644 --- a/vrtManager/tests/test_usage.py +++ b/vrtManager/tests/test_usage.py @@ -4,6 +4,7 @@ devices; otherwise over one second. The stats view runs in a sync worker, polled every 5 s by each open VM page.""" import unittest +from types import SimpleNamespace from unittest.mock import patch from django.conf import settings @@ -40,20 +41,24 @@ class FakeDomain: def info(self): return [self.state, 0, 0, 2, int(self.now() * 500_000_000)] # 0.5 s of CPU time per second - def blockStats(self, dev): - n = {"vda": 1, "vdb": 2}[dev] # disks are read by target name - written = -1 if self.unsupported and dev == "vdb" else int(self.now() * n * 3000) - return (0, int(self.now() * n * 1000), 0, written, 0) - - def interfaceStats(self, dev): - assert dev == "vnet0" - return (int(self.now() * 100), 0, 0, 0, int(self.now() * 50), 0, 0, 0) + def stats(self): + """domainListGetStats' counters: disks and NICs by name, as libvirt + lists them (the CD-ROM too); a counter the host does not report is left out.""" + stats = {"cpu.time": int(self.now() * 500_000_000), "block.count": 3, "net.count": 1} # 0.5 s of CPU a second + for i, (name, n) in enumerate((("vda", 1), ("vdb", 2), ("sda", 0))): + stats[f"block.{i}.name"] = name + stats[f"block.{i}.rd.bytes"] = int(self.now() * n * 1000) + if not (self.unsupported and name == "vdb"): + stats[f"block.{i}.wr.bytes"] = int(self.now() * n * 3000) + stats.update({"net.0.name": "vnet0", "net.0.rx.bytes": int(self.now() * 100), "net.0.tx.bytes": int(self.now() * 50)}) + return stats def instance(state, clock): proxy = wvmInstance.__new__(wvmInstance) # no connection: only these calls proxy._read_cache = None proxy.instance = FakeDomain(state, clock) + proxy.wvm = SimpleNamespace(domainListGetStats=lambda doms, kinds: [(doms[0], doms[0].stats())]) proxy._XMLDesc = lambda flags: XML proxy.host_info = ["x86_64", 0, 4] # 4 host CPUs proxy.get_status = lambda: state @@ -151,7 +156,8 @@ class UsageTestCase(unittest.TestCase): def restart(seconds): real_sleep(seconds) - domain.info = lambda: [1, 0, 0, 2, 0] # CPU time starts over + counters = domain.stats + domain.stats = lambda: {**counters(), "cpu.time": 0} # CPU time starts over self.time.sleep.side_effect = restart result = proxy.usage()