diff --git a/src/virtManager/config.py b/src/virtManager/config.py index d28b08311..d7da2369e 100644 --- a/src/virtManager/config.py +++ b/src/virtManager/config.py @@ -104,8 +104,39 @@ class vmmConfig(object): self._objects = [] self.support_threading = virtinst.support.support_threading() + + self.support_inspection = self.check_inspection(self.support_threading) + self._spice_error = None + def check_inspection(self, support_threading): + if not support_threading: + return False + + try: + # Check we can open the Python guestfs module. + from guestfs import GuestFS + g = GuestFS() + + # Check for the first version which fixed Python GIL bug. + version = g.version() + if version["major"] == 1: # major must be 1 + if version["minor"] == 8: + if version["release"] >= 6: # >= 1.8.6 + return True + elif version["minor"] == 10: + if version["release"] >= 1: # >= 1.10.1 + return True + elif version["minor"] == 11: + if version["release"] >= 2: # >= 1.11.2 + return True + elif version["minor"] >= 12: # >= 1.12, 1.13, etc. + return True + except: + pass + + return False + # General app wide helpers (gconf agnostic) def get_shutdown_icon_name(self): diff --git a/src/virtManager/engine.py b/src/virtManager/engine.py index dbbb8bd70..a310b8e29 100644 --- a/src/virtManager/engine.py +++ b/src/virtManager/engine.py @@ -236,6 +236,9 @@ class vmmEngine(vmmGObject): if not self.config.support_threading: logging.debug("Libvirt doesn't support threading, skipping.") + self.inspection_thread = None + self._create_inspection_thread() + # Counter keeping track of how many manager and details windows # are open. When it is decremented to 0, close the app or # keep running in system tray if enabled @@ -531,6 +534,20 @@ class vmmEngine(vmmGObject): logging.debug("Exiting app normally.") gtk.main_quit() + def _create_inspection_thread(self): + if not self.config.support_inspection: + logging.debug("No inspection thread because " + "libguestfs is too old, not available, " + "or libvirt is not thread safe.") + return + from virtManager.inspection import vmmInspection + self.inspection_thread = vmmInspection() + self.inspection_thread.daemon = True + self.inspection_thread.start() + self.connect("connection-added", self.inspection_thread.conn_added) + self.connect("connection-removed", self.inspection_thread.conn_removed) + return + def add_connection(self, uri): conn = self._check_connection(uri) if conn: diff --git a/src/virtManager/inspection.py b/src/virtManager/inspection.py new file mode 100644 index 000000000..99b225f6e --- /dev/null +++ b/src/virtManager/inspection.py @@ -0,0 +1,204 @@ +# +# Copyright (C) 2011 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. +# + +import time +from Queue import Queue, Empty +from threading import Thread + +from guestfs import GuestFS + +import logging + +class vmmInspection(Thread): + _name = "inspection thread" + _wait = 15 # seconds + + def __init__(self): + Thread.__init__(self, name=self._name) + self._q = Queue() + self._conns = dict() + self._vmseen = dict() + + # 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): + obj = ("vm_added") + self._q.put(obj) + + def run(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). + logging.debug("%s: waiting" % self._name) + time.sleep(self._wait) + + while True: + logging.debug("%s: ready" % self._name) + 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 + 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 vmuuid in conn.list_vm_uuids(): + if not (vmuuid in self._vmseen): + logging.debug("%s: processing started on '%s'" % + (self._name, vmuuid)) + try: + vm = conn.get_vm(vmuuid) + # Whether success or failure, we've "seen" this VM now. + self._vmseen[vmuuid] = True + self._process(conn, vm, vmuuid) + except: + logging.exception("%s: exception while processing '%s'" % + (self._name, vmuuid)) + logging.debug("%s: processing done on '%s'" % + (self._name, vmuuid)) + + def _process(self, conn_ignore, vm, vmuuid): + # Add the disks. Note they *must* be added with readonly flag set. + g = GuestFS() + for disk in vm.get_disk_devices(): + path = disk.path + driver_type = disk.driver_type + if (disk.type == "block" or disk.type == "file") and path != 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: %s: no operating systems found" % + (self._name, vmuuid)) + return + + # 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 + + # Added in libguestfs 1.9.13: + product_variant = None + if hasattr(g, "inspect_get_product_variant"): + 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 = 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)" % ( + self._name, + mp_dev[1], mp_dev[0])) + + filesystems_mounted = True + except: + logging.exception("%s: exception while mounting disks (ignored)" % + self._name) + + icon = None + apps = None + if filesystems_mounted: + # Added in libguestfs 1.11.12: + if hasattr(g, "inspect_get_icon"): + # 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)", + self._name, typ, distro, major_version, minor_version, + product_name) + logging.debug("%s: hostname: %s", self._name, hostname) + if icon: + logging.debug("%s: icon: %d bytes", self._name, len(icon)) + if apps: + logging.debug("%s: # apps: %d", self._name, len(apps)) diff --git a/src/virtManager/manager.py b/src/virtManager/manager.py index 0cffa2139..f10879d43 100644 --- a/src/virtManager/manager.py +++ b/src/virtManager/manager.py @@ -682,6 +682,9 @@ class vmmManager(vmmGObjectUI): self._append_vm(model, vm, connection) + if self.engine.inspection_thread: + self.engine.inspection_thread.vm_added() + def vm_removed(self, connection, uri_ignore, vmuuid): vmlist = self.widget("vm-list") model = vmlist.get_model()