1# Copyright (C) 2018 Red Hat, Inc. 2# 3# This work is licensed under the GNU GPLv2 or later. 4# See the COPYING file in the top-level directory. 5 6import re 7import time 8 9import libvirt 10 11from virtinst import log 12 13from ..baseclass import vmmGObject 14 15 16class _VMStatsRecord(object): 17 """ 18 Tracks a set of VM stats for a single timestamp 19 """ 20 def __init__(self, timestamp, 21 cpuTime, cpuTimeAbs, 22 cpuHostPercent, cpuGuestPercent, 23 curmem, currMemPercent, 24 diskRdBytes, diskWrBytes, 25 netRxBytes, netTxBytes): 26 self.timestamp = timestamp 27 self.cpuTime = cpuTime 28 self.cpuTimeAbs = cpuTimeAbs 29 self.cpuHostPercent = cpuHostPercent 30 self.cpuGuestPercent = cpuGuestPercent 31 self.curmem = curmem 32 self.currMemPercent = currMemPercent 33 self.diskRdKiB = diskRdBytes // 1024 34 self.diskWrKiB = diskWrBytes // 1024 35 self.netRxKiB = netRxBytes // 1024 36 self.netTxKiB = netTxBytes // 1024 37 38 # These are set in _VMStatsList.append_stats 39 self.diskRdRate = None 40 self.diskWrRate = None 41 self.netRxRate = None 42 self.netTxRate = None 43 44 45class _VMStatsList(vmmGObject): 46 """ 47 Tracks a list of VMStatsRecords for a single VM 48 """ 49 def __init__(self): 50 vmmGObject.__init__(self) 51 self._stats = [] 52 53 self.diskRdMaxRate = 10.0 54 self.diskWrMaxRate = 10.0 55 self.netRxMaxRate = 10.0 56 self.netTxMaxRate = 10.0 57 58 self.mem_stats_period_is_set = False 59 self.stats_disk_skip = [] 60 self.stats_net_skip = [] 61 62 def _cleanup(self): 63 pass 64 65 def append_stats(self, newstats): 66 expected = self.config.get_stats_history_length() 67 current = len(self._stats) 68 if current > expected: # pragma: no cover 69 del(self._stats[expected:current]) 70 71 def _calculate_rate(record_name): 72 ret = 0.0 73 if self._stats: 74 oldstats = self._stats[0] 75 ratediff = (getattr(newstats, record_name) - 76 getattr(oldstats, record_name)) 77 timediff = newstats.timestamp - oldstats.timestamp 78 ret = float(ratediff) / float(timediff) 79 return max(ret, 0.0) 80 81 newstats.diskRdRate = _calculate_rate("diskRdKiB") 82 newstats.diskWrRate = _calculate_rate("diskWrKiB") 83 newstats.netRxRate = _calculate_rate("netRxKiB") 84 newstats.netTxRate = _calculate_rate("netTxKiB") 85 86 self.diskRdMaxRate = max(newstats.diskRdRate, self.diskRdMaxRate) 87 self.diskWrMaxRate = max(newstats.diskWrRate, self.diskWrMaxRate) 88 self.netRxMaxRate = max(newstats.netRxRate, self.netRxMaxRate) 89 self.netTxMaxRate = max(newstats.netTxRate, self.netTxMaxRate) 90 91 self._stats.insert(0, newstats) 92 93 def get_record(self, record_name): 94 if not self._stats: 95 return 0 96 return getattr(self._stats[0], record_name) 97 98 def get_vector(self, record_name, limit, ceil=100.0): 99 vector = [] 100 statslen = self.config.get_stats_history_length() + 1 101 if limit is not None: 102 statslen = min(statslen, limit) 103 104 for i in range(statslen): 105 if i < len(self._stats): 106 vector.append(getattr(self._stats[i], record_name) / ceil) 107 else: 108 vector.append(0) 109 return vector 110 111 def get_in_out_vector(self, name1, name2, limit, ceil): 112 return (self.get_vector(name1, limit, ceil=ceil), 113 self.get_vector(name2, limit, ceil=ceil)) 114 115 116class vmmStatsManager(vmmGObject): 117 """ 118 Class for polling statistics 119 """ 120 def __init__(self): 121 vmmGObject.__init__(self) 122 self._vm_stats = {} 123 self._latest_all_stats = {} 124 125 self._all_stats_supported = True 126 self._net_stats_supported = True 127 self._disk_stats_supported = True 128 self._disk_stats_lxc_supported = True 129 self._mem_stats_supported = True 130 131 132 def _cleanup(self): 133 for statslist in self._vm_stats.values(): 134 statslist.cleanup() 135 self._latest_all_stats = None 136 137 138 ###################### 139 # CPU stats handling # 140 ###################### 141 142 def _old_cpu_stats_helper(self, vm): 143 info = vm.get_backend().info() 144 state = info[0] 145 guestcpus = info[3] 146 cpuTimeAbs = info[4] 147 return state, guestcpus, cpuTimeAbs 148 149 def _sample_cpu_stats(self, vm, allstats): 150 timestamp = time.time() 151 if (not vm.is_active() or 152 not self.config.get_stats_enable_cpu_poll()): 153 return 0, 0, 0, 0, timestamp 154 155 cpuTime = 0 156 cpuHostPercent = 0 157 cpuGuestPercent = 0 158 prevTimestamp = self.get_vm_statslist(vm).get_record("timestamp") 159 prevCpuTime = self.get_vm_statslist(vm).get_record("cpuTimeAbs") 160 161 if allstats: 162 state = allstats.get("state.state", 0) 163 guestcpus = allstats.get("vcpu.current", 0) 164 cpuTimeAbs = allstats.get("cpu.time", 0) 165 timestamp = allstats.get("virt-manager.timestamp") 166 else: 167 state, guestcpus, cpuTimeAbs = self._old_cpu_stats_helper(vm) 168 169 is_offline = (state in [libvirt.VIR_DOMAIN_SHUTOFF, 170 libvirt.VIR_DOMAIN_CRASHED]) 171 if is_offline: 172 guestcpus = 0 173 cpuTimeAbs = 0 174 175 cpuTime = cpuTimeAbs - prevCpuTime 176 if not is_offline: 177 hostcpus = vm.conn.host_active_processor_count() 178 179 pcentbase = ( 180 ((cpuTime) * 100.0) / 181 ((timestamp - prevTimestamp) * 1000.0 * 1000.0 * 1000.0)) 182 cpuHostPercent = pcentbase / hostcpus 183 # Under RHEL-5.9 using a XEN HV guestcpus can be 0 during shutdown 184 # so play safe and check it. 185 cpuGuestPercent = guestcpus > 0 and pcentbase / guestcpus or 0 186 187 cpuHostPercent = max(0.0, min(100.0, cpuHostPercent)) 188 cpuGuestPercent = max(0.0, min(100.0, cpuGuestPercent)) 189 190 return cpuTime, cpuTimeAbs, cpuHostPercent, cpuGuestPercent, timestamp 191 192 193 ###################### 194 # net stats handling # 195 ###################### 196 197 def _old_net_stats_helper(self, vm, dev): 198 statslist = self.get_vm_statslist(vm) 199 try: 200 io = vm.get_backend().interfaceStats(dev) 201 if io: 202 rx = io[0] 203 tx = io[4] 204 return rx, tx 205 except libvirt.libvirtError as err: # pragma: no cover 206 if vm.conn.support.is_error_nosupport(err): 207 log.debug("conn does not support interfaceStats") 208 self._net_stats_supported = False 209 return 0, 0 210 211 log.debug("Error in interfaceStats for '%s' dev '%s': %s", 212 vm.get_name(), dev, err) 213 if vm.is_active(): 214 log.debug("Adding %s to skip list", dev) 215 statslist.stats_net_skip.append(dev) 216 else: 217 log.debug("Aren't running, don't add to skiplist") 218 219 return 0, 0 # pragma: no cover 220 221 def _sample_net_stats(self, vm, allstats): 222 rx = 0 223 tx = 0 224 statslist = self.get_vm_statslist(vm) 225 if (not self._net_stats_supported or 226 not vm.is_active() or 227 not self.config.get_stats_enable_net_poll()): 228 statslist.stats_net_skip = [] 229 return rx, tx 230 231 if allstats: 232 for key in allstats.keys(): # pragma: no cover 233 if re.match(r"net.[0-9]+.rx.bytes", key): 234 rx += allstats[key] 235 if re.match(r"net.[0-9]+.tx.bytes", key): 236 tx += allstats[key] 237 return rx, tx 238 239 for iface in vm.get_interface_devices_norefresh(): 240 dev = iface.target_dev 241 if not dev: 242 continue # pragma: no cover 243 if dev in statslist.stats_net_skip: 244 continue # pragma: no cover 245 246 devrx, devtx = self._old_net_stats_helper(vm, dev) 247 rx += devrx 248 tx += devtx 249 250 return rx, tx 251 252 253 ####################### 254 # disk stats handling # 255 ####################### 256 257 def _old_disk_stats_helper(self, vm, dev): 258 statslist = self.get_vm_statslist(vm) 259 try: 260 io = vm.get_backend().blockStats(dev) 261 if io: 262 rd = io[1] 263 wr = io[3] 264 return rd, wr 265 except libvirt.libvirtError as err: # pragma: no cover 266 if vm.conn.support.is_error_nosupport(err): 267 log.debug("conn does not support blockStats") 268 self._disk_stats_supported = False 269 return 0, 0 270 271 log.debug("Error in blockStats for '%s' dev '%s': %s", 272 vm.get_name(), dev, err) 273 if vm.is_active(): 274 log.debug("Adding %s to skip list", dev) 275 statslist.stats_disk_skip.append(dev) 276 else: 277 log.debug("Aren't running, don't add to skiplist") 278 279 return 0, 0 # pragma: no cover 280 281 def _sample_disk_stats(self, vm, allstats): 282 rd = 0 283 wr = 0 284 statslist = self.get_vm_statslist(vm) 285 if (not self._disk_stats_supported or 286 not vm.is_active() or 287 not self.config.get_stats_enable_disk_poll()): 288 statslist.stats_disk_skip = [] 289 return rd, wr 290 291 if allstats: 292 for key in allstats.keys(): 293 if re.match(r"block.[0-9]+.rd.bytes", key): 294 rd += allstats[key] 295 if re.match(r"block.[0-9]+.wr.bytes", key): 296 wr += allstats[key] 297 return rd, wr 298 299 # LXC has a special blockStats method 300 if vm.conn.is_lxc() and self._disk_stats_lxc_supported: 301 try: 302 io = vm.get_backend().blockStats('') 303 if io: 304 rd = io[1] 305 wr = io[3] 306 return rd, wr 307 except libvirt.libvirtError as e: # pragma: no cover 308 log.debug("LXC style disk stats not supported: %s", e) 309 self._disk_stats_lxc_supported = False 310 311 for disk in vm.get_disk_devices_norefresh(): 312 dev = disk.target 313 if not dev: 314 continue # pragma: no cover 315 if dev in statslist.stats_disk_skip: 316 continue # pragma: no cover 317 318 diskrd, diskwr = self._old_disk_stats_helper(vm, dev) 319 rd += diskrd 320 wr += diskwr 321 322 return rd, wr 323 324 325 ######################### 326 # memory stats handling # 327 ######################### 328 329 def _set_mem_stats_period(self, vm): 330 # QEMU requires to explicitly enable memory stats polling per VM 331 # if we want fine grained memory stats 332 if not vm.conn.support.conn_mem_stats_period(): 333 return 334 335 # Only works for virtio balloon 336 if not any([b for b in vm.get_xmlobj().devices.memballoon if 337 b.model == "virtio"]): 338 return # pragma: no cover 339 340 try: 341 secs = 5 342 vm.get_backend().setMemoryStatsPeriod(secs, 343 libvirt.VIR_DOMAIN_AFFECT_LIVE) 344 except Exception as e: # pragma: no cover 345 log.debug("Error setting memstats period: %s", e) 346 347 def _old_mem_stats_helper(self, vm): 348 totalmem = 1 349 curmem = 0 350 try: 351 stats = vm.get_backend().memoryStats() 352 totalmem = stats.get("actual", 1) 353 curmem = max(0, totalmem - stats.get("unused", totalmem)) 354 except libvirt.libvirtError as err: # pragma: no cover 355 if vm.conn.support.is_error_nosupport(err): 356 log.debug("conn does not support memoryStats") 357 self._mem_stats_supported = False 358 else: 359 log.debug("Error reading mem stats: %s", err) 360 361 return totalmem, curmem 362 363 def _sample_mem_stats(self, vm, allstats): 364 statslist = self.get_vm_statslist(vm) 365 if (not self._mem_stats_supported or 366 not vm.is_active() or 367 not self.config.get_stats_enable_memory_poll()): 368 statslist.mem_stats_period_is_set = False 369 return 0, 0 370 371 if statslist.mem_stats_period_is_set is False: 372 self._set_mem_stats_period(vm) 373 statslist.mem_stats_period_is_set = True 374 375 if allstats: 376 totalmem = allstats.get("balloon.current", 1) 377 curmem = max(0, 378 totalmem - allstats.get("balloon.unused", totalmem)) 379 else: 380 totalmem, curmem = self._old_mem_stats_helper(vm) 381 382 currMemPercent = (curmem / float(totalmem)) * 100 383 currMemPercent = max(0.0, min(currMemPercent, 100.0)) 384 385 return currMemPercent, curmem 386 387 388 #################### 389 # alltats handling # 390 #################### 391 392 def _get_all_stats(self, conn): 393 if not self._all_stats_supported: 394 return {} 395 396 statflags = 0 397 if self.config.get_stats_enable_cpu_poll(): 398 statflags |= libvirt.VIR_DOMAIN_STATS_STATE 399 statflags |= libvirt.VIR_DOMAIN_STATS_CPU_TOTAL 400 statflags |= libvirt.VIR_DOMAIN_STATS_VCPU 401 if self.config.get_stats_enable_memory_poll(): 402 statflags |= libvirt.VIR_DOMAIN_STATS_BALLOON 403 if self.config.get_stats_enable_disk_poll(): 404 statflags |= libvirt.VIR_DOMAIN_STATS_BLOCK 405 if self.config.get_stats_enable_net_poll(): 406 statflags |= libvirt.VIR_DOMAIN_STATS_INTERFACE 407 if statflags == 0: 408 return {} # pragma: no cover 409 410 ret = {} 411 try: 412 timestamp = time.time() 413 rawallstats = conn.get_backend().getAllDomainStats(statflags, 0) 414 415 # Reformat the output to be a bit more friendly 416 for dom, domallstats in rawallstats: 417 domallstats["virt-manager.timestamp"] = timestamp 418 ret[dom.UUIDString()] = domallstats 419 except libvirt.libvirtError as err: 420 if conn.support.is_error_nosupport(err): 421 log.debug("conn does not support getAllDomainStats()") 422 self._all_stats_supported = False 423 else: # pragma: no cover 424 log.debug("Error call getAllDomainStats(): %s", err) 425 return ret 426 427 428 ############## 429 # Public API # 430 ############## 431 432 def refresh_vm_stats(self, vm): 433 domallstats = self._latest_all_stats.get(vm.get_uuid(), None) 434 435 (cpuTime, cpuTimeAbs, cpuHostPercent, cpuGuestPercent, timestamp) = \ 436 self._sample_cpu_stats(vm, domallstats) 437 currMemPercent, curmem = self._sample_mem_stats(vm, domallstats) 438 diskRdBytes, diskWrBytes = self._sample_disk_stats(vm, domallstats) 439 netRxBytes, netTxBytes = self._sample_net_stats(vm, domallstats) 440 441 newstats = _VMStatsRecord( 442 timestamp, cpuTime, cpuTimeAbs, 443 cpuHostPercent, cpuGuestPercent, 444 curmem, currMemPercent, 445 diskRdBytes, diskWrBytes, 446 netRxBytes, netTxBytes) 447 self.get_vm_statslist(vm).append_stats(newstats) 448 449 def cache_all_stats(self, conn): 450 self._latest_all_stats = self._get_all_stats(conn) 451 452 def get_vm_statslist(self, vm): 453 if vm.get_name() not in self._vm_stats: 454 self._vm_stats[vm.get_name()] = _VMStatsList() 455 return self._vm_stats[vm.get_name()] 456