#
# Copyright (C) 2011, 2013 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301 USA.
#

from Queue import Queue, Empty
from threading import Thread
import logging
import os
import re

from guestfs import GuestFS  # pylint: disable=import-error

from virtManager.baseclass import vmmGObject
from virtManager.domain import vmmInspectionData


class vmmInspection(vmmGObject):
    # Can't find a way to make Thread release our reference
    _leak_check = False

    def __init__(self):
        vmmGObject.__init__(self)

        self._thread = Thread(name="inspection thread", target=self._run)
        self._thread.daemon = True
        self._wait = 5 * 1000  # 5 seconds

        self._q = Queue()
        self._conns = {}
        self._vmseen = {}
        self._cached_data = {}

    def _cleanup(self):
        self._thread = None
        self._q = Queue()
        self._conns = {}
        self._vmseen = {}
        self._cached_data = {}

    # Called by the main thread whenever a connection is added or
    # removed.  We tell the inspection thread, so it can track
    # connections.
    def conn_added(self, engine_ignore, conn):
        obj = ("conn_added", conn)
        self._q.put(obj)

    def conn_removed(self, engine_ignore, uri):
        obj = ("conn_removed", uri)
        self._q.put(obj)

    # Called by the main thread whenever a VM is added to vmlist.
    def vm_added(self, conn, connkey):
        ignore = conn
        ignore = connkey
        obj = ("vm_added")
        self._q.put(obj)

    def start(self):
        # Wait a few seconds before we do anything.  This prevents
        # inspection from being a burden for initial virt-manager
        # interactivity (although it shouldn't affect interactivity at
        # all).
        def cb():
            self._thread.start()
            return 0

        logging.debug("waiting")
        self.timeout_add(self._wait, cb)

    def _run(self):
        while True:
            self._process_queue()
            self._process_vms()

    # Process everything on the queue.  If the queue is empty when
    # called, block.
    def _process_queue(self):
        first_obj = self._q.get()
        self._process_queue_item(first_obj)
        self._q.task_done()
        try:
            while True:
                obj = self._q.get(False)
                self._process_queue_item(obj)
                self._q.task_done()
        except Empty:
            pass

    def _process_queue_item(self, obj):
        if obj[0] == "conn_added":
            conn = obj[1]
            if conn and not (conn.is_remote()):
                uri = conn.get_uri()
                self._conns[uri] = conn
                conn.connect("vm-added", self.vm_added)
        elif obj[0] == "conn_removed":
            uri = obj[1]
            del self._conns[uri]
        elif obj[0] == "vm_added":
            # Nothing - just a signal for the inspection thread to wake up.
            pass

    # Any VMs we've not seen yet?  If so, process them.
    def _process_vms(self):
        for conn in self._conns.itervalues():
            for vm in conn.list_vms():
                if not conn.is_active():
                    break

                def set_inspection_error(vm):
                    data = vmmInspectionData()
                    data.error = True
                    self._set_vm_inspection_data(vm, data)

                vmuuid = vm.get_uuid()
                prettyvm = vmuuid
                try:
                    prettyvm = conn.get_uri() + ":" + vm.get_name()

                    if vmuuid in self._vmseen:
                        data = self._cached_data.get(vmuuid)
                        if not data:
                            continue

                        if vm.inspection != data:
                            logging.debug("Found cached data for %s", prettyvm)
                            self._set_vm_inspection_data(vm, data)
                        continue

                    # Whether success or failure, we've "seen" this VM now.
                    self._vmseen[vmuuid] = True
                    try:
                        data = self._process(conn, vm, vmuuid)
                        if data:
                            self._set_vm_inspection_data(vm, data)
                        else:
                            set_inspection_error(vm)
                    except:
                        set_inspection_error(vm)
                        raise
                except:
                    logging.exception("%s: exception while processing",
                                      prettyvm)

    def _process(self, conn, vm, vmuuid):
        if re.search(r"^guestfs-", vm.get_name()):
            logging.debug("ignore libvirt/guestfs temporary VM %s",
                          vm.get_name())
            return None

        g = GuestFS(close_on_exit=False)
        prettyvm = conn.get_uri() + ":" + vm.get_name()
        ignore = vmuuid

        disks = []
        for disk in vm.get_disk_devices():
            if (disk.path and
                (disk.type == "block" or disk.type == "file") and
                not disk.device == "cdrom"):
                disks.append(disk)

        if not disks:
            logging.debug("%s: nothing to inspect", prettyvm)
            return None

        # Add the disks.  Note they *must* be added with readonly flag set.
        for disk in disks:
            path = disk.path
            driver_type = disk.driver_type

            if not (os.path.exists(path) and os.access(path, os.R_OK)):
                logging.debug("%s: cannot access '%s', skipping inspection",
                              prettyvm, path)
                return None

            g.add_drive_opts(path, readonly=1, format=driver_type)

        g.launch()

        # Inspect the operating system.
        roots = g.inspect_os()
        if len(roots) == 0:
            logging.debug("%s: no operating systems found", prettyvm)
            return None

        # Arbitrarily pick the first root device.
        root = roots[0]

        # Inspection results.
        typ = g.inspect_get_type(root)  # eg. "linux"
        distro = g.inspect_get_distro(root)  # eg. "fedora"
        major_version = g.inspect_get_major_version(root)  # eg. 14
        minor_version = g.inspect_get_minor_version(root)  # eg. 0
        hostname = g.inspect_get_hostname(root)  # string
        product_name = g.inspect_get_product_name(root)  # string
        product_variant = g.inspect_get_product_variant(root)  # string

        # For inspect_list_applications and inspect_get_icon we
        # require that the guest filesystems are mounted.  However
        # don't fail if this is not possible (I'm looking at you,
        # FreeBSD).
        filesystems_mounted = False
        try:
            # Mount up the disks, like guestfish --ro -i.

            # Sort keys by length, shortest first, so that we end up
            # mounting the filesystems in the correct order.
            mps = list(g.inspect_get_mountpoints(root))
            def compare(a, b):
                if len(a[0]) > len(b[0]):
                    return 1
                elif len(a[0]) == len(b[0]):
                    return 0
                else:
                    return -1
            mps.sort(compare)

            for mp_dev in mps:
                try:
                    g.mount_ro(mp_dev[1], mp_dev[0])
                except:
                    logging.exception("%s: exception mounting %s on %s "
                                      "(ignored)",
                                      prettyvm, mp_dev[1], mp_dev[0])

            filesystems_mounted = True
        except:
            logging.exception("%s: exception while mounting disks (ignored)",
                              prettyvm)

        icon = None
        apps = None
        if filesystems_mounted:
            # string containing PNG data
            icon = g.inspect_get_icon(root, favicon=0, highquality=1)
            if icon == "":
                icon = None

            # Inspection applications.
            apps = g.inspect_list_applications(root)

        # Force the libguestfs handle to close right now.
        del g

        # Log what we found.
        logging.debug("%s: detected operating system: %s %s %d.%d (%s)",
                      prettyvm, typ, distro, major_version, minor_version,
                      product_name)
        logging.debug("hostname: %s", hostname)
        if icon:
            logging.debug("icon: %d bytes", len(icon))
        if apps:
            logging.debug("# apps: %d", len(apps))

        data = vmmInspectionData()
        data.type = str(type)
        data.distro = str(distro)
        data.major_version = int(major_version)
        data.minor_version = int(minor_version)
        data.hostname = str(hostname)
        data.product_name = str(product_name)
        data.product_variant = str(product_variant)
        data.icon = icon
        data.applications = list(apps)
        data.error = False

        return data

    def _set_vm_inspection_data(self, vm, data):
        vm.inspection = data
        vm.inspection_data_updated()
        self._cached_data[vm.get_uuid()] = data
