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