
from __future__ import absolute_import
from __future__ import division

import libvirt
import six
from six.moves import map

"""
Support for VM and host statistics sampling.
"""

from collections import defaultdict, deque, namedtuple
import logging
import os
import re
import threading
import time

from vdsm import hugepages
from vdsm import numa
from vdsm import utils
import vdsm.common.time
from vdsm.config import config
from vdsm.constants import P_VDSM_RUN
from vdsm.host import api as hostapi
from vdsm.virt.utils import ExpiringCache


_THP_STATE_PATH = '/sys/kernel/mm/transparent_hugepage/enabled'
if not os.path.exists(_THP_STATE_PATH):
    _THP_STATE_PATH = '/sys/kernel/mm/redhat_transparent_hugepage/enabled'
_METRICS_ENABLED = config.getboolean('metrics', 'enabled')


class TotalCpuSample(object):
    def __init__(self):
        with open('/proc/stat') as f:
            self.user, userNice, self.sys, self.idle = \
                map(int, f.readline().split()[1:5])
        self.user += userNice


class CpuCoreSample(object):
    CPU_CORE_STATS_PATTERN = re.compile(r'cpu(\d+)\s+(.*)')

    def __init__(self):
        self.coresSample = {}
        with open('/proc/stat') as src:
            for line in src:
                match = self.CPU_CORE_STATS_PATTERN.match(line)
                if match:
                    coreSample = {}
                    user, userNice, sys, idle = \
                        map(int, match.group(2).split()[0:4])
                    coreSample['user'] = user
                    coreSample['userNice'] = userNice
                    coreSample['sys'] = sys
                    coreSample['idle'] = idle
                    self.coresSample[match.group(1)] = coreSample

    def getCoreSample(self, coreId):
        strCoreId = str(coreId)
        return self.coresSample.get(strCoreId)


class NumaNodeMemorySample(object):
    def __init__(self):
        self.nodesMemSample = {}
        numaTopology = numa.topology()
        for nodeIndex in numaTopology:
            nodeMemSample = {}
            if len(numaTopology) == 1:
                idx = -1
            else:
                idx = int(nodeIndex)
            memInfo = numa.memory_by_cell(idx)
            nodeMemSample['memFree'] = memInfo['free']
            nodeMemSample['memPercent'] = 100
            if int(memInfo['total']) != 0:
                nodeMemSample['memPercent'] = 100 - \
                    int(100.0 * int(memInfo['free']) // int(memInfo['total']))
            self.nodesMemSample[nodeIndex] = nodeMemSample


class PidCpuSample(object):

    def __init__(self, pid):
        with open('/proc/%s/stat' % pid) as stat:
            self.user, self.sys = \
                map(int, stat.read().split()[13:15])


class HostSample(object):
    MONITORED_PATHS = ['/tmp', '/var/log', P_VDSM_RUN]

    def _getDiskStats(self):
        d = {}
        for p in self.MONITORED_PATHS:
            free = 0
            try:
                stat = os.statvfs(p)
                free = stat.f_bavail * stat.f_bsize // (2 ** 20)
            except:
                pass
            d[p] = {'free': str(free)}
        return d

    def __init__(self, pid):
        self.timestamp = time.time()
        self.pidcpu = PidCpuSample(pid)
        self.ncpus = os.sysconf('SC_NPROCESSORS_ONLN')
        self.totcpu = TotalCpuSample()
        meminfo = utils.readMemInfo()
        freeOrCached = (meminfo['MemFree'] +
                        meminfo['Cached'] + meminfo['Buffers'])
        self.memUsed = 100 - int(100.0 * (freeOrCached) / meminfo['MemTotal'])
        self.anonHugePages = meminfo.get('AnonHugePages', 0) // 1024
        try:
            with open('/proc/loadavg') as loadavg:
                self.cpuLoad = loadavg.read().split()[1]
        except:
            self.cpuLoad = '0.0'
        self.diskStats = self._getDiskStats()
        try:
            with open(_THP_STATE_PATH) as f:
                s = f.read()
                self.thpState = s[s.index('[') + 1:s.index(']')]
        except:
            self.thpState = 'never'
        self.hugepages = hugepages.state()
        self.cpuCores = CpuCoreSample()
        self.numaNodeMem = NumaNodeMemorySample()


_MINIMUM_SAMPLES = 1


class SampleWindow(object):

    def __init__(self, size, timefn=time.time):
        if size < _MINIMUM_SAMPLES:
            raise ValueError("window size must be not less than %i" %
                             _MINIMUM_SAMPLES)

        self._samples = deque(maxlen=size)
        self._timefn = timefn

    def append(self, value):
        timestamp = self._timefn()
        self._samples.append((timestamp, value))

    def stats(self):
        if len(self._samples) < 2:
            return None, None, None

        first_timestamp, first_sample = self._samples[0]
        last_timestamp, last_sample = self._samples[-1]

        elapsed_time = last_timestamp - first_timestamp
        return first_sample, last_sample, elapsed_time

    def last(self, nth=1):
        if len(self._samples) < nth:
            return None, None
        return self._samples[-nth]


_StatsSample = namedtuple('StatsSample',
                          ['first_value', 'last_value',
                           'interval', 'stats_age'])


class StatsSample(_StatsSample):
    def is_empty(self):
        return (
            self.first_value is None and
            self.last_value is None and
            self.interval is None
        )


class StatsCache(object):

    _log = logging.getLogger("virt.sampling.StatsCache")

    def __init__(self, clock=vdsm.common.time.monotonic_time):
        self._clock = clock
        self._lock = threading.Lock()
        self._samples = SampleWindow(size=2, timefn=self._clock)
        self._last_sample_time = 0
        self._vm_last_timestamp = defaultdict(int)

    def add(self, vmid):
        with self._lock:
            self._vm_last_timestamp[vmid] = self._clock()

    def remove(self, vmid):
        with self._lock:
            del self._vm_last_timestamp[vmid]

    def get(self, vmid):
        with self._lock:
            first_batch, last_batch, interval = self._samples.stats()
            stats_age = self._clock() - self._vm_last_timestamp[vmid]

            if first_batch is None:
                return StatsSample(None, None, None, stats_age)

            first_sample = first_batch.get(vmid)
            last_sample = last_batch.get(vmid)

            if first_sample is None or last_sample is None:
                return StatsSample(None, None, None, stats_age)

            return StatsSample(first_sample, last_sample,
                               interval, stats_age)

    def get_batch(self):
        with self._lock:
            first_batch, last_batch, interval = self._samples.stats()

            if first_batch is None:
                return None

            ts = self._clock()
            return {
                vm_id: StatsSample(
                    first_batch[vm_id], last_batch[vm_id], interval,
                    ts - self._vm_last_timestamp[vm_id]
                )
                for vm_id in last_batch if (vm_id in first_batch and
                                            vm_id in self._vm_last_timestamp)
            }

    def clock(self):
        return self._clock()

    def put(self, bulk_stats, monotonic_ts):
        with self._lock:
            last_sample_time = self._last_sample_time
            if monotonic_ts >= last_sample_time:
                self._samples.append(bulk_stats)
                self._last_sample_time = monotonic_ts

                self._update_ts(bulk_stats, monotonic_ts)
            else:
                self._log.warning(
                    'dropped stale old sample: sampled %f stored %f',
                    monotonic_ts, last_sample_time)

    def _update_ts(self, bulk_stats, monotonic_ts):
        for vmid in bulk_stats:
            self._vm_last_timestamp[vmid] = monotonic_ts


stats_cache = StatsCache()


_TTL = 40.0


BULK_STATS_TYPES = (
    libvirt.VIR_DOMAIN_STATS_STATE |
    libvirt.VIR_DOMAIN_STATS_CPU_TOTAL |
    libvirt.VIR_DOMAIN_STATS_BALLOON |
    libvirt.VIR_DOMAIN_STATS_VCPU |
    libvirt.VIR_DOMAIN_STATS_INTERFACE |
    libvirt.VIR_DOMAIN_STATS_BLOCK
)


class VMBulkstatsMonitor(object):
    def __init__(self, conn, get_vms, stats_cache,
                 stats_types=BULK_STATS_TYPES, ttl=_TTL):
        self._conn = conn
        self._get_vms = get_vms
        self._stats_cache = stats_cache
        self._stats_types = stats_types
        self._skip_doms = ExpiringCache(ttl)
        self._log = logging.getLogger("virt.sampling.VMBulkstatsMonitor")

    def __call__(self):
        log_status = True
        timestamp = self._stats_cache.clock()
        acquired = self._sampling.acquire(blocking=False)
        fast_path = acquired and not self._skip_doms
        try:
            if fast_path:
                bulk_stats = self._conn.getAllDomainStats(
                    stats=self._stats_types, flags=flags)
            else:
                doms = self._get_responsive_doms()
                if doms:
                    bulk_stats = self._conn.domainListGetStats(
                        doms, stats=self._stats_types, flags=flags)
                else:
                    bulk_stats = []
        except Exception:
            self._log.exception("vm sampling failed")
            log_status = False
        else:
            self._stats_cache.put(_translate(bulk_stats), timestamp)
        finally:
            if acquired:
                self._sampling.release()
        if log_status:
            self._log.debug(
                'sampled timestamp %r elapsed %.3f acquired %r domains %s',
                timestamp, self._stats_cache.clock() - timestamp, acquired,
                'all' if fast_path else len(doms))

    def _get_responsive_doms(self):
        vms = self._get_vms()
        doms = []
        for vm_id, vm_obj in six.iteritems(vms):
            to_skip = self._skip_doms.get(vm_id, False)
            if to_skip:
                continue
            elif not vm_obj.isDomainReadyForCommands():
                self._skip_doms[vm_id] = True
            else:
                doms.append(vm_obj._dom._dom)
        return doms


HOST_STATS_AVERAGING_WINDOW = 2


host_samples = SampleWindow(size=HOST_STATS_AVERAGING_WINDOW)


class HostMonitor(object):

    def __init__(self, samples=host_samples, cif=None):
        self._samples = samples
        self._pid = os.getpid()
        self._cif = cif

    def __call__(self):
        sample = HostSample(self._pid)
        self._samples.append(sample)

        if self._cif and _METRICS_ENABLED:
            stats = hostapi.get_stats(self._cif, self._samples.stats())
            hostapi.send_metrics(stats)


def _translate(bulk_stats):
    return dict((dom.UUIDString(), stats)
                for dom, stats in bulk_stats)
