mirror of
https://github.com/virt-manager/virt-manager.git
synced 2026-08-04 18:33:33 -05:00
Make it explicit that all uses of this is actually the object name. We already leaked this abstraction in several places so better to make it explicit. This also communicates to users that this is a field that is not immutable so it shouldn't be used as a unique key Signed-off-by: Cole Robinson <crobinso@redhat.com>
456 lines
15 KiB
Python
456 lines
15 KiB
Python
# Copyright (C) 2018 Red Hat, Inc.
|
|
#
|
|
# This work is licensed under the GNU GPLv2 or later.
|
|
# See the COPYING file in the top-level directory.
|
|
|
|
import re
|
|
import time
|
|
|
|
import libvirt
|
|
|
|
from virtinst import log
|
|
|
|
from ..baseclass import vmmGObject
|
|
|
|
|
|
class _VMStatsRecord(object):
|
|
"""
|
|
Tracks a set of VM stats for a single timestamp
|
|
"""
|
|
def __init__(self, timestamp,
|
|
cpuTime, cpuTimeAbs,
|
|
cpuHostPercent, cpuGuestPercent,
|
|
curmem, currMemPercent,
|
|
diskRdBytes, diskWrBytes,
|
|
netRxBytes, netTxBytes):
|
|
self.timestamp = timestamp
|
|
self.cpuTime = cpuTime
|
|
self.cpuTimeAbs = cpuTimeAbs
|
|
self.cpuHostPercent = cpuHostPercent
|
|
self.cpuGuestPercent = cpuGuestPercent
|
|
self.curmem = curmem
|
|
self.currMemPercent = currMemPercent
|
|
self.diskRdKiB = diskRdBytes // 1024
|
|
self.diskWrKiB = diskWrBytes // 1024
|
|
self.netRxKiB = netRxBytes // 1024
|
|
self.netTxKiB = netTxBytes // 1024
|
|
|
|
# These are set in _VMStatsList.append_stats
|
|
self.diskRdRate = None
|
|
self.diskWrRate = None
|
|
self.netRxRate = None
|
|
self.netTxRate = None
|
|
|
|
|
|
class _VMStatsList(vmmGObject):
|
|
"""
|
|
Tracks a list of VMStatsRecords for a single VM
|
|
"""
|
|
def __init__(self):
|
|
vmmGObject.__init__(self)
|
|
self._stats = []
|
|
|
|
self.diskRdMaxRate = 10.0
|
|
self.diskWrMaxRate = 10.0
|
|
self.netRxMaxRate = 10.0
|
|
self.netTxMaxRate = 10.0
|
|
|
|
self.mem_stats_period_is_set = False
|
|
self.stats_disk_skip = []
|
|
self.stats_net_skip = []
|
|
|
|
def _cleanup(self):
|
|
pass
|
|
|
|
def append_stats(self, newstats):
|
|
expected = self.config.get_stats_history_length()
|
|
current = len(self._stats)
|
|
if current > expected:
|
|
del(self._stats[expected:current])
|
|
|
|
def _calculate_rate(record_name):
|
|
ret = 0.0
|
|
if self._stats:
|
|
oldstats = self._stats[0]
|
|
ratediff = (getattr(newstats, record_name) -
|
|
getattr(oldstats, record_name))
|
|
timediff = newstats.timestamp - oldstats.timestamp
|
|
ret = float(ratediff) / float(timediff)
|
|
return max(ret, 0.0)
|
|
|
|
newstats.diskRdRate = _calculate_rate("diskRdKiB")
|
|
newstats.diskWrRate = _calculate_rate("diskWrKiB")
|
|
newstats.netRxRate = _calculate_rate("netRxKiB")
|
|
newstats.netTxRate = _calculate_rate("netTxKiB")
|
|
|
|
self.diskRdMaxRate = max(newstats.diskRdRate, self.diskRdMaxRate)
|
|
self.diskWrMaxRate = max(newstats.diskWrRate, self.diskWrMaxRate)
|
|
self.netRxMaxRate = max(newstats.netRxRate, self.netRxMaxRate)
|
|
self.netTxMaxRate = max(newstats.netTxRate, self.netTxMaxRate)
|
|
|
|
self._stats.insert(0, newstats)
|
|
|
|
def get_record(self, record_name):
|
|
if not self._stats:
|
|
return 0
|
|
return getattr(self._stats[0], record_name)
|
|
|
|
def get_vector(self, record_name, limit, ceil=100.0):
|
|
vector = []
|
|
statslen = self.config.get_stats_history_length() + 1
|
|
if limit is not None:
|
|
statslen = min(statslen, limit)
|
|
|
|
for i in range(statslen):
|
|
if i < len(self._stats):
|
|
vector.append(getattr(self._stats[i], record_name) / ceil)
|
|
else:
|
|
vector.append(0)
|
|
return vector
|
|
|
|
def get_in_out_vector(self, name1, name2, limit, ceil):
|
|
if ceil is None:
|
|
ceil = max(self.get_record(name1), self.get_record(name2), 10.0)
|
|
return (self.get_vector(name1, limit, ceil=ceil),
|
|
self.get_vector(name2, limit, ceil=ceil))
|
|
|
|
|
|
class vmmStatsManager(vmmGObject):
|
|
"""
|
|
Class for polling statistics
|
|
"""
|
|
def __init__(self):
|
|
vmmGObject.__init__(self)
|
|
self._vm_stats = {}
|
|
self._latest_all_stats = {}
|
|
|
|
self._all_stats_supported = True
|
|
self._net_stats_supported = True
|
|
self._disk_stats_supported = True
|
|
self._disk_stats_lxc_supported = True
|
|
self._mem_stats_supported = True
|
|
|
|
|
|
def _cleanup(self):
|
|
self._latest_all_stats = None
|
|
|
|
|
|
######################
|
|
# CPU stats handling #
|
|
######################
|
|
|
|
def _old_cpu_stats_helper(self, vm):
|
|
info = vm.get_backend().info()
|
|
state = info[0]
|
|
guestcpus = info[3]
|
|
cpuTimeAbs = info[4]
|
|
return state, guestcpus, cpuTimeAbs
|
|
|
|
def _sample_cpu_stats(self, vm, allstats):
|
|
timestamp = time.time()
|
|
if (not vm.is_active() or
|
|
not self.config.get_stats_enable_cpu_poll()):
|
|
return 0, 0, 0, 0, timestamp
|
|
|
|
cpuTime = 0
|
|
cpuHostPercent = 0
|
|
cpuGuestPercent = 0
|
|
prevTimestamp = self.get_vm_statslist(vm).get_record("timestamp")
|
|
prevCpuTime = self.get_vm_statslist(vm).get_record("cpuTimeAbs")
|
|
|
|
if allstats:
|
|
state = allstats.get("state.state", 0)
|
|
guestcpus = allstats.get("vcpu.current", 0)
|
|
cpuTimeAbs = allstats.get("cpu.time", 0)
|
|
timestamp = allstats.get("virt-manager.timestamp")
|
|
else:
|
|
state, guestcpus, cpuTimeAbs = self._old_cpu_stats_helper(vm)
|
|
|
|
is_offline = (state in [libvirt.VIR_DOMAIN_SHUTOFF,
|
|
libvirt.VIR_DOMAIN_CRASHED])
|
|
if is_offline:
|
|
guestcpus = 0
|
|
cpuTimeAbs = 0
|
|
|
|
cpuTime = cpuTimeAbs - prevCpuTime
|
|
if not is_offline:
|
|
hostcpus = vm.conn.host_active_processor_count()
|
|
|
|
pcentbase = (
|
|
((cpuTime) * 100.0) /
|
|
((timestamp - prevTimestamp) * 1000.0 * 1000.0 * 1000.0))
|
|
cpuHostPercent = pcentbase / hostcpus
|
|
# Under RHEL-5.9 using a XEN HV guestcpus can be 0 during shutdown
|
|
# so play safe and check it.
|
|
cpuGuestPercent = guestcpus > 0 and pcentbase / guestcpus or 0
|
|
|
|
cpuHostPercent = max(0.0, min(100.0, cpuHostPercent))
|
|
cpuGuestPercent = max(0.0, min(100.0, cpuGuestPercent))
|
|
|
|
return cpuTime, cpuTimeAbs, cpuHostPercent, cpuGuestPercent, timestamp
|
|
|
|
|
|
######################
|
|
# net stats handling #
|
|
######################
|
|
|
|
def _old_net_stats_helper(self, vm, dev):
|
|
statslist = self.get_vm_statslist(vm)
|
|
try:
|
|
io = vm.get_backend().interfaceStats(dev)
|
|
if io:
|
|
rx = io[0]
|
|
tx = io[4]
|
|
return rx, tx
|
|
except libvirt.libvirtError as err:
|
|
if vm.conn.support.is_error_nosupport(err):
|
|
log.debug("conn does not support interfaceStats")
|
|
self._net_stats_supported = False
|
|
return 0, 0
|
|
|
|
log.debug("Error in interfaceStats for '%s' dev '%s': %s",
|
|
vm.get_name(), dev, err)
|
|
if vm.is_active():
|
|
log.debug("Adding %s to skip list", dev)
|
|
statslist.stats_net_skip.append(dev)
|
|
else:
|
|
log.debug("Aren't running, don't add to skiplist")
|
|
|
|
return 0, 0
|
|
|
|
def _sample_net_stats(self, vm, allstats):
|
|
rx = 0
|
|
tx = 0
|
|
statslist = self.get_vm_statslist(vm)
|
|
if (not self._net_stats_supported or
|
|
not vm.is_active() or
|
|
not self.config.get_stats_enable_net_poll()):
|
|
statslist.stats_net_skip = []
|
|
return rx, tx
|
|
|
|
if allstats:
|
|
for key in allstats.keys():
|
|
if re.match(r"net.[0-9]+.rx.bytes", key):
|
|
rx += allstats[key]
|
|
if re.match(r"net.[0-9]+.tx.bytes", key):
|
|
tx += allstats[key]
|
|
return rx, tx
|
|
|
|
for iface in vm.get_interface_devices_norefresh():
|
|
dev = iface.target_dev
|
|
if not dev:
|
|
continue
|
|
if dev in statslist.stats_net_skip:
|
|
continue
|
|
|
|
devrx, devtx = self._old_net_stats_helper(vm, dev)
|
|
rx += devrx
|
|
tx += devtx
|
|
|
|
return rx, tx
|
|
|
|
|
|
#######################
|
|
# disk stats handling #
|
|
#######################
|
|
|
|
def _old_disk_stats_helper(self, vm, dev):
|
|
statslist = self.get_vm_statslist(vm)
|
|
try:
|
|
io = vm.get_backend().blockStats(dev)
|
|
if io:
|
|
rd = io[1]
|
|
wr = io[3]
|
|
return rd, wr
|
|
except libvirt.libvirtError as err:
|
|
if vm.conn.support.is_error_nosupport(err):
|
|
log.debug("conn does not support blockStats")
|
|
self._disk_stats_supported = False
|
|
return 0, 0
|
|
|
|
log.debug("Error in blockStats for '%s' dev '%s': %s",
|
|
vm.get_name(), dev, err)
|
|
if vm.is_active():
|
|
log.debug("Adding %s to skip list", dev)
|
|
statslist.stats_disk_skip.append(dev)
|
|
else:
|
|
log.debug("Aren't running, don't add to skiplist")
|
|
|
|
return 0, 0
|
|
|
|
def _sample_disk_stats(self, vm, allstats):
|
|
rd = 0
|
|
wr = 0
|
|
statslist = self.get_vm_statslist(vm)
|
|
if (not self._disk_stats_supported or
|
|
not vm.is_active() or
|
|
not self.config.get_stats_enable_disk_poll()):
|
|
statslist.stats_disk_skip = []
|
|
return rd, wr
|
|
|
|
if allstats:
|
|
for key in allstats.keys():
|
|
if re.match(r"block.[0-9]+.rd.bytes", key):
|
|
rd += allstats[key]
|
|
if re.match(r"block.[0-9]+.wr.bytes", key):
|
|
wr += allstats[key]
|
|
return rd, wr
|
|
|
|
# LXC has a special blockStats method
|
|
if vm.conn.is_lxc() and self._disk_stats_lxc_supported:
|
|
try:
|
|
io = vm.get_backend().blockStats('')
|
|
if io:
|
|
rd = io[1]
|
|
wr = io[3]
|
|
return rd, wr
|
|
except libvirt.libvirtError as e:
|
|
log.debug("LXC style disk stats not supported: %s", e)
|
|
self._disk_stats_lxc_supported = False
|
|
|
|
for disk in vm.get_disk_devices_norefresh():
|
|
dev = disk.target
|
|
if not dev:
|
|
continue
|
|
if dev in statslist.stats_disk_skip:
|
|
continue
|
|
|
|
diskrd, diskwr = self._old_disk_stats_helper(vm, dev)
|
|
rd += diskrd
|
|
wr += diskwr
|
|
|
|
return rd, wr
|
|
|
|
|
|
#########################
|
|
# memory stats handling #
|
|
#########################
|
|
|
|
def _set_mem_stats_period(self, vm):
|
|
# QEMU requires to explicitly enable memory stats polling per VM
|
|
# if we want fine grained memory stats
|
|
if not vm.conn.support.conn_mem_stats_period():
|
|
return
|
|
|
|
# Only works for virtio balloon
|
|
if not any([b for b in vm.get_xmlobj().devices.memballoon if
|
|
b.model == "virtio"]):
|
|
return
|
|
|
|
try:
|
|
secs = 5
|
|
vm.get_backend().setMemoryStatsPeriod(secs,
|
|
libvirt.VIR_DOMAIN_AFFECT_LIVE)
|
|
except Exception as e:
|
|
log.debug("Error setting memstats period: %s", e)
|
|
|
|
def _old_mem_stats_helper(self, vm):
|
|
totalmem = 1
|
|
curmem = 0
|
|
try:
|
|
stats = vm.get_backend().memoryStats()
|
|
totalmem = stats.get("actual", 1)
|
|
curmem = max(0, totalmem - stats.get("unused", totalmem))
|
|
except libvirt.libvirtError as err:
|
|
if vm.conn.support.is_error_nosupport(err):
|
|
log.debug("conn does not support memoryStats")
|
|
self._mem_stats_supported = False
|
|
else:
|
|
log.debug("Error reading mem stats: %s", err)
|
|
|
|
return totalmem, curmem
|
|
|
|
def _sample_mem_stats(self, vm, allstats):
|
|
statslist = self.get_vm_statslist(vm)
|
|
if (not self._mem_stats_supported or
|
|
not vm.is_active() or
|
|
not self.config.get_stats_enable_memory_poll()):
|
|
statslist.mem_stats_period_is_set = False
|
|
return 0, 0
|
|
|
|
if statslist.mem_stats_period_is_set is False:
|
|
self._set_mem_stats_period(vm)
|
|
statslist.mem_stats_period_is_set = True
|
|
|
|
if allstats:
|
|
totalmem = allstats.get("balloon.current", 1)
|
|
curmem = max(0,
|
|
totalmem - allstats.get("balloon.unused", totalmem))
|
|
else:
|
|
totalmem, curmem = self._old_mem_stats_helper(vm)
|
|
|
|
currMemPercent = (curmem / float(totalmem)) * 100
|
|
currMemPercent = max(0.0, min(currMemPercent, 100.0))
|
|
|
|
return currMemPercent, curmem
|
|
|
|
|
|
####################
|
|
# alltats handling #
|
|
####################
|
|
|
|
def _get_all_stats(self, conn):
|
|
if not self._all_stats_supported:
|
|
return {}
|
|
|
|
statflags = 0
|
|
if self.config.get_stats_enable_cpu_poll():
|
|
statflags |= libvirt.VIR_DOMAIN_STATS_STATE
|
|
statflags |= libvirt.VIR_DOMAIN_STATS_CPU_TOTAL
|
|
statflags |= libvirt.VIR_DOMAIN_STATS_VCPU
|
|
if self.config.get_stats_enable_memory_poll():
|
|
statflags |= libvirt.VIR_DOMAIN_STATS_BALLOON
|
|
if self.config.get_stats_enable_disk_poll():
|
|
statflags |= libvirt.VIR_DOMAIN_STATS_BLOCK
|
|
if self.config.get_stats_enable_net_poll():
|
|
statflags |= libvirt.VIR_DOMAIN_STATS_INTERFACE
|
|
if statflags == 0:
|
|
return {}
|
|
|
|
ret = {}
|
|
try:
|
|
timestamp = time.time()
|
|
rawallstats = conn.get_backend().getAllDomainStats(statflags, 0)
|
|
|
|
# Reformat the output to be a bit more friendly
|
|
for dom, domallstats in rawallstats:
|
|
domallstats["virt-manager.timestamp"] = timestamp
|
|
ret[dom.UUIDString()] = domallstats
|
|
except libvirt.libvirtError as err:
|
|
if conn.support.is_error_nosupport(err):
|
|
log.debug("conn does not support getAllDomainStats()")
|
|
self._all_stats_supported = False
|
|
else:
|
|
log.debug("Error call getAllDomainStats(): %s", err)
|
|
return ret
|
|
|
|
|
|
##############
|
|
# Public API #
|
|
##############
|
|
|
|
def refresh_vm_stats(self, vm):
|
|
domallstats = self._latest_all_stats.get(vm.get_uuid(), None)
|
|
|
|
(cpuTime, cpuTimeAbs, cpuHostPercent, cpuGuestPercent, timestamp) = \
|
|
self._sample_cpu_stats(vm, domallstats)
|
|
currMemPercent, curmem = self._sample_mem_stats(vm, domallstats)
|
|
diskRdBytes, diskWrBytes = self._sample_disk_stats(vm, domallstats)
|
|
netRxBytes, netTxBytes = self._sample_net_stats(vm, domallstats)
|
|
|
|
newstats = _VMStatsRecord(
|
|
timestamp, cpuTime, cpuTimeAbs,
|
|
cpuHostPercent, cpuGuestPercent,
|
|
curmem, currMemPercent,
|
|
diskRdBytes, diskWrBytes,
|
|
netRxBytes, netTxBytes)
|
|
self.get_vm_statslist(vm).append_stats(newstats)
|
|
|
|
def cache_all_stats(self, conn):
|
|
self._latest_all_stats = self._get_all_stats(conn)
|
|
|
|
def get_vm_statslist(self, vm):
|
|
if vm.get_name() not in self._vm_stats:
|
|
self._vm_stats[vm.get_name()] = _VMStatsList()
|
|
return self._vm_stats[vm.get_name()]
|