#
# Copyright 2012 Red Hat, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Implementation of Inspector abstraction for libvirt."""

from lxml import etree
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import units
import six

from ceilometer.compute.pollsters import util
from ceilometer.compute.virt import inspector as virt_inspector
from ceilometer.i18n import _LW, _LE, _

libvirt = None

LOG = logging.getLogger(__name__)

OPTS = [
    cfg.StrOpt('libvirt_type',
               default='kvm',
               choices=['kvm', 'lxc', 'qemu', 'uml', 'xen'],
               help='Libvirt domain type.'),
    cfg.StrOpt('libvirt_uri',
               default='',
               help='Override the default libvirt URI '
                    '(which is dependent on libvirt_type).'),
]

CONF = cfg.CONF
CONF.register_opts(OPTS)


def retry_on_disconnect(function):
    def decorator(self, *args, **kwargs):
        try:
            return function(self, *args, **kwargs)
        except ImportError:
            # NOTE(sileht): in case of libvirt failed to be imported
            raise
        except libvirt.libvirtError as e:
            if (e.get_error_code() in (libvirt.VIR_ERR_SYSTEM_ERROR,
                                       libvirt.VIR_ERR_INTERNAL_ERROR) and
                e.get_error_domain() in (libvirt.VIR_FROM_REMOTE,
                                         libvirt.VIR_FROM_RPC)):
                LOG.debug('Connection to libvirt broken')
                self.connection = None
                return function(self, *args, **kwargs)
            else:
                raise
    return decorator


class LibvirtInspector(virt_inspector.Inspector):

    per_type_uris = dict(uml='uml:///system', xen='xen:///', lxc='lxc:///')

    def __init__(self):
        self._connection = None

    @property
    def connection(self):
        if not self._connection:
            global libvirt
            if libvirt is None:
                libvirt = __import__('libvirt')

            uri = (CONF.libvirt_uri or
                   self.per_type_uris.get(CONF.libvirt_type, 'qemu:///system'))
            LOG.debug('Connecting to libvirt: %s', uri)
            self._connection = libvirt.openReadOnly(uri)

        return self._connection

    def check_sanity(self):
        if not self.connection:
            raise virt_inspector.NoSanityException()

    @retry_on_disconnect
    def _lookup_by_uuid(self, instance):
        instance_name = util.instance_name(instance)
        try:
            return self.connection.lookupByUUIDString(instance.id)
        except Exception as ex:
            if not libvirt or not isinstance(ex, libvirt.libvirtError):
                raise virt_inspector.InspectorException(six.text_type(ex))
            error_code = ex.get_error_code()
            if (error_code in (libvirt.VIR_ERR_SYSTEM_ERROR,
                               libvirt.VIR_ERR_INTERNAL_ERROR) and
                ex.get_error_domain() in (libvirt.VIR_FROM_REMOTE,
                                          libvirt.VIR_FROM_RPC)):
                raise
            msg = _("Error from libvirt while looking up instance "
                    "<name=%(name)s, id=%(id)s>: "
                    "[Error Code %(error_code)s] "
                    "%(ex)s") % {'name': instance_name,
                                 'id': instance.id,
                                 'error_code': error_code,
                                 'ex': ex}
            raise virt_inspector.InstanceNotFoundException(msg)

    def inspect_cpus(self, instance):
        domain = self._get_domain_not_shut_off_or_raise(instance)
        dom_info = domain.info()
        return virt_inspector.CPUStats(number=dom_info[3], time=dom_info[4])

    def inspect_cpu_l3_cache(self, instance):
        domain = self._lookup_by_uuid(instance)
        try:
            stats = self.connection.domainListGetStats(
                [domain], libvirt.VIR_DOMAIN_STATS_PERF)
            perf = stats[0][1]
            usage = perf["perf.cmt"]
            return virt_inspector.CPUL3CacheUsageStats(l3_cache_usage=usage)
        except AttributeError as e:
            msg = _('Perf is not supported by current version of libvirt, and '
                    'failed to inspect l3 cache usage of %(instance_uuid)s, '
                    'can not get info from libvirt: %(error)s') % {
                'instance_uuid': instance.id, 'error': e}
            raise virt_inspector.NoDataException(msg)
        # domainListGetStats might launch an exception if the method or
        # cmt perf event is not supported by the underlying hypervisor
        # being used by libvirt.
        except libvirt.libvirtError as e:
            msg = _('Failed to inspect l3 cache usage of %(instance_uuid)s, '
                    'can not get info from libvirt: %(error)s') % {
                'instance_uuid': instance.id, 'error': e}
            raise virt_inspector.NoDataException(msg)

    def _get_domain_not_shut_off_or_raise(self, instance):
        instance_name = util.instance_name(instance)
        domain = self._lookup_by_uuid(instance)

        state = domain.info()[0]
        if state == libvirt.VIR_DOMAIN_SHUTOFF:
            msg = _('Failed to inspect data of instance '
                    '<name=%(name)s, id=%(id)s>, '
                    'domain state is SHUTOFF.') % {
                'name': instance_name, 'id': instance.id}
            raise virt_inspector.InstanceShutOffException(msg)

        return domain

    def inspect_vnics(self, instance):
        domain = self._get_domain_not_shut_off_or_raise(instance)

        tree = etree.fromstring(domain.XMLDesc(0))
        for iface in tree.findall('devices/interface'):
            target = iface.find('target')
            if target is not None:
                name = target.get('dev')
            else:
                continue
            mac = iface.find('mac')
            if mac is not None:
                mac_address = mac.get('address')
            else:
                continue
            fref = iface.find('filterref')
            if fref is not None:
                fref = fref.get('filter')

            params = dict((p.get('name').lower(), p.get('value'))
                          for p in iface.findall('filterref/parameter'))
            interface = virt_inspector.Interface(name=name, mac=mac_address,
                                                 fref=fref, parameters=params)
            dom_stats = domain.interfaceStats(name)
            stats = virt_inspector.InterfaceStats(rx_bytes=dom_stats[0],
                                                  rx_packets=dom_stats[1],
                                                  tx_bytes=dom_stats[4],
                                                  tx_packets=dom_stats[5])
            yield (interface, stats)

    def inspect_disks(self, instance):
        domain = self._get_domain_not_shut_off_or_raise(instance)

        tree = etree.fromstring(domain.XMLDesc(0))
        for device in filter(
                bool,
                [target.get("dev")
                 for target in tree.findall('devices/disk/target')]):
            disk = virt_inspector.Disk(device=device)
            block_stats = domain.blockStats(device)
            stats = virt_inspector.DiskStats(read_requests=block_stats[0],
                                             read_bytes=block_stats[1],
                                             write_requests=block_stats[2],
                                             write_bytes=block_stats[3],
                                             errors=block_stats[4])
            yield (disk, stats)

    def inspect_memory_usage(self, instance, duration=None):
        instance_name = util.instance_name(instance)
        domain = self._get_domain_not_shut_off_or_raise(instance)

        try:
            memory_stats = domain.memoryStats()
            if (memory_stats and
                    memory_stats.get('available') and
                    memory_stats.get('unused')):
                memory_used = (memory_stats.get('available') -
                               memory_stats.get('unused'))
                # Stat provided from libvirt is in KB, converting it to MB.
                memory_used = memory_used / units.Ki
                return virt_inspector.MemoryUsageStats(usage=memory_used)
            else:
                msg = _('Failed to inspect memory usage of instance '
                        '<name=%(name)s, id=%(id)s>, '
                        'can not get info from libvirt.') % {
                    'name': instance_name, 'id': instance.id}
                raise virt_inspector.InstanceNoDataException(msg)
        # memoryStats might launch an exception if the method is not supported
        # by the underlying hypervisor being used by libvirt.
        except libvirt.libvirtError as e:
            msg = _('Failed to inspect memory usage of %(instance_uuid)s, '
                    'can not get info from libvirt: %(error)s') % {
                'instance_uuid': instance.id, 'error': e}
            raise virt_inspector.NoDataException(msg)

    def inspect_disk_info(self, instance):
        domain = self._get_domain_not_shut_off_or_raise(instance)
        tree = etree.fromstring(domain.XMLDesc(0))
        for disk in tree.findall('devices/disk'):
            disk_type = disk.get('type')
            if disk_type:
                if disk_type == 'network':
                    LOG.warning(
                        _LW('Inspection disk usage of network disk '
                            '%(instance_uuid)s unsupported by libvirt') % {
                            'instance_uuid': instance.id})
                    continue
                target = disk.find('target')
                device = target.get('dev')
                if device:
                    dsk = virt_inspector.Disk(device=device)
                    block_info = domain.blockInfo(device)
                    info = virt_inspector.DiskInfo(capacity=block_info[0],
                                                   allocation=block_info[1],
                                                   physical=block_info[2])
                    yield (dsk, info)

    def inspect_memory_resident(self, instance, duration=None):
        domain = self._get_domain_not_shut_off_or_raise(instance)
        memory = domain.memoryStats()['rss'] / units.Ki
        return virt_inspector.MemoryResidentStats(resident=memory)

    def inspect_memory_bandwidth(self, instance, duration=None):
        domain = self._get_domain_not_shut_off_or_raise(instance)

        try:
            stats = self.connection.domainListGetStats(
                [domain], libvirt.VIR_DOMAIN_STATS_PERF)
            perf = stats[0][1]
            return virt_inspector.MemoryBandwidthStats(total=perf["perf.mbmt"],
                                                       local=perf["perf.mbml"])
        except AttributeError as e:
            msg = _('Perf is not supported by current version of libvirt, and '
                    'failed to inspect memory bandwidth of %(instance_uuid)s, '
                    'can not get info from libvirt: %(error)s') % {
                'instance_uuid': instance.id, 'error': e}
            raise virt_inspector.NoDataException(msg)
        # domainListGetStats might launch an exception if the method or
        # mbmt/mbml perf event is not supported by the underlying hypervisor
        # being used by libvirt.
        except libvirt.libvirtError as e:
            msg = _('Failed to inspect memory bandwidth of %(instance_uuid)s, '
                    'can not get info from libvirt: %(error)s') % {
                'instance_uuid': instance.id, 'error': e}
            raise virt_inspector.NoDataException(msg)

    def inspect_perf_events(self, instance, duration=None):
        domain = self._get_domain_not_shut_off_or_raise(instance)

        try:
            stats = self.connection.domainListGetStats(
                [domain], libvirt.VIR_DOMAIN_STATS_PERF)
            perf = stats[0][1]
            return virt_inspector.PerfEventsStats(
                cpu_cycles=perf["perf.cpu_cycles"],
                instructions=perf["perf.instructions"],
                cache_references=perf["perf.cache_references"],
                cache_misses=perf["perf.cache_misses"])
        except AttributeError as e:
            msg = _LE('Perf is not supported by current version of libvirt, '
                      'and failed to inspect perf events of '
                      '%(instance_uuid)s, can not get info from libvirt: '
                      '%(error)s') % {
                'instance_uuid': instance.id, 'error': e}
            raise virt_inspector.NoDataException(msg)
        # domainListGetStats might launch an exception if the method or
        # mbmt/mbml perf event is not supported by the underlying hypervisor
        # being used by libvirt.
        except libvirt.libvirtError as e:
            msg = _LE('Failed to inspect perf events of %(instance_uuid)s, '
                      'can not get info from libvirt: %(error)s') % {
                'instance_uuid': instance.id, 'error': e}
            raise virt_inspector.NoDataException(msg)
