diff --git a/src/virtManager/config.py b/src/virtManager/config.py index 0d8d5ddb7..f4ba70e16 100644 --- a/src/virtManager/config.py +++ b/src/virtManager/config.py @@ -20,6 +20,8 @@ import gconf import gtk.gdk +from virtManager.keyring import * + class vmmConfig: def __init__(self, appname, gconf_dir, glade_dir, icon_dir): self.appname = appname @@ -30,6 +32,10 @@ class vmmConfig: self.glade_dir = glade_dir self.icon_dir = icon_dir + # We don;t create it straight away, since we don't want + # to block the app pending user authorizaation to access + # the keyring + self.keyring = None self.status_icons = { "blocked": gtk.gdk.pixbuf_new_from_file_at_size(self.get_icon_dir() + "/state_blocked.png", 18, 18), @@ -153,3 +159,48 @@ class vmmConfig: self.conf.notify_add(self.conf_dir + "/stats/history-length", callback) + def get_secret_name(self, vm): + return "vm-console-" + vm.get_uuid() + + def clear_console_password(self, vm): + id = self.conf.get_int(self.conf_dir + "/console/passwords/" + vm.get_uuid()) + + if id != None: + if self.keyring == None: + self.keyring = vmmKeyring() + + self.keyring.clear_secret(id) + self.conf.unset(self.conf_dir + "/console/passwords/" + vm.get_uuid()) + + def get_console_password(self, vm): + id = self.conf.get_int(self.conf_dir + "/console/passwords/" + vm.get_uuid()) + if id != None: + if self.keyring == None: + try: + self.keyring = vmmKeyring() + except: + print "Unable to access keyring" + return "" + + secret = self.keyring.get_secret(id) + if secret != None and secret.get_name() == self.get_secret_name(vm): + # XXX validate attributes + return secret.get_secret() + return "" + + def set_console_password(self, vm, password): + if self.keyring == None: + try: + self.keyring = vmmKeyring() + except: + print "Unable to access keyring" + return + + # Nb, we don't bother to check if there is an existing + # secret, because gnome-keyring auto-replaces an existing + # one if the attributes match - which they will since UUID + # is our unique key + + secret = vmmSecret(self.get_secret_name(vm), password, { "uuid" : vm.get_uuid(), "hvuri": vm.get_connection().get_uri() }) + id = self.keyring.add_secret(secret) + self.conf.set_int(self.conf_dir + "/console/passwords/" + vm.get_uuid(), id) diff --git a/src/virtManager/console.py b/src/virtManager/console.py index 752d475b7..1b2e654f9 100644 --- a/src/virtManager/console.py +++ b/src/virtManager/console.py @@ -119,8 +119,7 @@ class vmmConsole(gobject.GObject): return 0 def _vnc_disconnected(self, src): - self.window.get_widget("console-auth-password").set_text("") - self.window.get_widget("console-pages").set_current_page(2) + self.activate_auth_page() def try_login(self, src=None): password = self.window.get_widget("console-auth-password").get_text() @@ -132,22 +131,43 @@ class vmmConsole(gobject.GObject): #print protocol + "://" + host + ":" + str(port) if protocol != "vnc": - print "Activate inactive" - self.window.get_widget("console-pages").set_curent_page(0) + self.activate_unavailable_page() return if not(self.vncViewer.is_connected()): self.vncViewer.connect_to_host(host, port) if self.vncViewer.is_authenticated(): - self.window.get_widget("console-pages").set_current_page(3) + self.activate_viewer_page() elif password and (self.vncViewer.authenticate(password) == 1): - self.window.get_widget("console-pages").set_current_page(3) + if self.window.get_widget("console-auth-remember").get_active(): + self.config.set_console_password(self.vm, password) + else: + self.config.clear_console_password(self.vm) + self.activate_viewer_page() self.vncViewer.activate() else: - self.window.get_widget("console-auth-password").set_text("") - self.window.get_widget("console-pages").set_current_page(2) + self.activate_auth_page() + + def activate_unavailable_page(self): + self.window.get_widget("console-pages").set_current_page(0) + + def activate_screenshot_page(self): + self.window.get_widget("console-pages").set_current_page(1) + + def activate_auth_page(self): + pw = self.config.get_console_password(self.vm) + self.window.get_widget("console-auth-password").set_text(pw) + if pw != None and pw != "": + self.window.get_widget("console-auth-remember").set_active(True) + else: + self.window.get_widget("console-auth-remember").set_active(False) + self.window.get_widget("console-pages").set_current_page(2) + + def activate_viewer_page(self): + self.window.get_widget("console-pages").set_current_page(3) + def control_vm_shutdown(self, src): status = self.vm.status() if not(status in [ libvirt.VIR_DOMAIN_SHUTDOWN, libvirt.VIR_DOMAIN_SHUTOFF, libvirt.VIR_DOMAIN_CRASHED ]): @@ -246,9 +266,9 @@ class vmmConsole(gobject.GObject): cr.show_text(overlay) self.window.get_widget("console-screenshot").set_from_pixmap(screenshot, None) - self.window.get_widget("console-pages").set_current_page(1) + self.activate_screenshot_page() else: - self.window.get_widget("console-pages").set_current_page(0) + self.activate_unavailable_page() else: self.try_login() except: diff --git a/src/virtManager/keyring.py b/src/virtManager/keyring.py new file mode 100644 index 000000000..7845121ed --- /dev/null +++ b/src/virtManager/keyring.py @@ -0,0 +1,234 @@ +# +# Copyright (C) 2006 Red Hat, Inc. +# Copyright (C) 2006 Daniel P. Berrange +# +# 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., 675 Mass Ave, Cambridge, MA 02139, USA. +# +# +# There is no python binding for keyring in FC5 so we use +# ctypes to do the magic. This code is scary, but works +# pretty well...so far +# +# XXX audit this code for memory leaks. The gnome-keyring API +# docs are non-existant so i've no clue what bits are the callers +# responsibility to free() :-( + +from ctypes import * +import gobject + +from virtManager.secret import * + +class vmmKeyring: + # Map to GnomeKeyringAttribute struct + # XXX lame that we have 32 & 64 bit variant + # need to get the Union stuff working for the + # 'value' field which should solve the padding + # problems automagically + class Attribute64(Structure): + _fields_ =[('name', c_char_p), + ('type', c_int), + ('pad', c_int), + ('value', c_char_p)] + class Attribute32(Structure): + _fields_ =[('name', c_char_p), + ('type', c_int), + ('value', c_char_p)] + + # Hack to map to GArray struct in glib + class GArray(Structure): + _fields_ = [('data', c_char_p), + ('len', c_uint)] + + def __init__(self): + self.loop = gobject.MainLoop() + + # Load the two libs we need to play with + self.glib = cdll.LoadLibrary("libglib-2.0.so") + self.krlib = cdll.LoadLibrary("libgnome-keyring.so") + + # Declare the callback type + cbtype = CFUNCTYPE(c_void_p, c_int, c_char_p, c_void_p) + self.cb = cbtype(self._get_default_keyring_complete) + + # This gets filled out by callback with keyring name + self.keyring = None + + # Get the keyring name + f = self.krlib.gnome_keyring_get_default_keyring(self.cb, None, None) + # Block until complete + # XXX lame - blocks whole UI + self.loop.run() + + # User might have denied access + if self.keyring == None: + raise "Cannot access default keyring" + + + s = vmmSecret("Hello! World", "hus!h!hush", { "fdosdo": "bar1", "wi1zz": 3, "wow": 222 }) + sid = self.add_secret(s) + self.secrets = {} + + sout = self.get_secret(sid) + + + def add_secret(self, secret): + # We need to store the attributes in an array + g_array_new = self.glib.g_array_new + g_array_new.restype = c_void_p + + # XXX remove this lame 32/64 bit fork + if sizeof(c_void_p) == 4: + attrs = g_array_new(c_int(0), c_int(0), sizeof(c_char_p) + sizeof(c_int) + sizeof(c_char_p)) + else: + attrs = g_array_new(c_int(0), c_int(0), sizeof(c_char_p) + sizeof(c_int) + sizeof(c_int) + sizeof(c_char_p)) + + # Key a hold of them so they not immediately garbage collected + saveAttrs = {} + for key in secret.list_attributes(): + # Add all attributes to array + a = None + # XXX remove this lame 32/64 bit fork + if sizeof(c_void_p) == 4: + a = vmmKeyring.Attribute32(name= c_char_p(key), + type= c_int(0), + value= c_char_p(str(secret.get_attribute(key)))) + else: + a = vmmKeyring.Attribute64(name= c_char_p(key), + type= c_int(0), + pad= c_int(0), + value= c_char_p(str(secret.get_attribute(key)))) + saveAttrs[key] = a + self.glib.g_array_append_vals(attrs, byref(a), 1) + + # Declare callback type + cbaddtype = CFUNCTYPE(c_void_p, c_int, c_int, POINTER(c_int)) + self.cbadd = cbaddtype(self._add_secret_complete) + + # Fetch handle to our function + creator = self.krlib.gnome_keyring_item_create + creator.restype = c_void_p + # Callback will populate id of the secret in this + id = c_int(-1) + + # Now add the secret + creator(None, c_int(0), c_char_p(secret.get_name()), attrs, c_char_p(secret.get_secret()), c_int(1), self.cbadd, pointer(id), None) + # Block until compelte + self.loop.run() + + # Release attributes no longer neede + self.glib.g_array_free(attrs) + + return id.value + + def get_secret(self, id): + # Declare the callback type + cbgetinfotype = CFUNCTYPE(c_void_p, c_int, c_void_p, POINTER(c_int)) + self.cbgetinfo = cbgetinfotype(self._get_item_info_complete) + + # Fetch the method we want to call + getinfo = self.krlib.gnome_keyring_item_get_info + getinfo.restype = c_void_p + + # We need this in callback + i = c_int(id) + + # Fetch the basic info + p = getinfo(c_char_p(self.keyring), c_int(id), self.cbgetinfo, pointer(i), None) + # Block until done + self.loop.run() + if self.secrets.has_key(id): + # Declare callback type + cbgetattrstype = CFUNCTYPE(c_void_p, c_int, POINTER(vmmKeyring.GArray), POINTER(c_int)) + self.cbgetattrs = cbgetattrstype(self._get_item_attrs_complete) + + # Declare function we wnt to call to get attributes + getattrs = self.krlib.gnome_keyring_item_get_attributes + getattrs.restype = c_void_p + + # Fetch the attrs + getattrs(c_char_p(self.keyring), c_int(id), self.cbgetattrs, pointer(i), None) + # Block until done + self.loop.run() + + secret = self.secrets[id] + del self.secrets[id] + return secret + else: + return None + + def clear_secret(self, id): + # Declare the callback type + cbdeletetype = CFUNCTYPE(c_void_p, c_int, c_void_p) + self.cbdelete = cbdeletetype(self._delete_item_complete) + + # Fetch the method we want to call + getinfo = self.krlib.gnome_keyring_item_delete + getinfo.restype = c_void_p + + # Fetch the basic info + p = getinfo(c_char_p(self.keyring), c_int(id), self.cbdelete, None, None) + # Block until done + self.loop.run() + + def _get_default_keyring_complete(self, status, name, data): + if status != 0: + self.keyring = None + self.loop.quit() + return + # Save name of default keyring somewhere safe + if name == None: + name = "" + self.keyring = name + self.loop.quit() + + def _add_secret_complete(self, status, id, data): + if status != 0: + data.contents.value = -1 + self.loop.quit() + return + data.contents.value = id + self.loop.quit() + + def _delete_item_complete(self, status, data): + self.loop.quit() + + def _get_item_info_complete(self, status, info, data=None): + if status != 0: + self.loop.quit() + return + + getname = self.krlib.gnome_keyring_item_info_get_display_name + getname.restype = c_char_p + + getsecret = self.krlib.gnome_keyring_item_info_get_secret + getsecret.restype = c_char_p + + name = getname(info) + secret = getsecret(info) + + self.secrets[data.contents.value] = vmmSecret(name, secret) + + self.loop.quit() + + def _get_item_attrs_complete(self, status, attrs, data=None): + if status != 0: + self.loop.quit() + return + + # XXX @#%$&(#%@ glib has a macro for accessing + # elements in array which can obviously can't use + # from python. Figure out nasty pointer magic here... + + self.loop.quit() diff --git a/src/virtManager/secret.py b/src/virtManager/secret.py new file mode 100644 index 000000000..4f7a183dd --- /dev/null +++ b/src/virtManager/secret.py @@ -0,0 +1,48 @@ +# +# Copyright (C) 2006 Red Hat, Inc. +# Copyright (C) 2006 Daniel P. Berrange +# +# 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., 675 Mass Ave, Cambridge, MA 02139, USA. +# + + +class vmmSecret: + + def __init__(self, name, secret=None, attributes={}): + + self.name = name + self.secret = secret + self.attributes = attributes + + def set_secret(self, data): + self.secret = data + + def get_secret(self): + return self.secret + + def get_name(self): + return self.name + + def add_attribute(self, key, value): + if type(value) != str: + value = str(value) + + self.attributes[key] = value + + def list_attributes(self): + return self.attributes.keys() + + def get_attribute(self, key): + return self.attributes[key] diff --git a/virt-manager.spec.in b/virt-manager.spec.in index b3438b56f..d541406d5 100644 --- a/virt-manager.spec.in +++ b/virt-manager.spec.in @@ -13,11 +13,20 @@ URL: http://people.redhat.com/berrange/virt-manager/ Source0: %{name}-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) +# Any modern python should do Requires: python +# These two are just the oldest version tested Requires: pygtk2 >= 1.99.12-6 Requires: gnome-python2-gconf >= 1.99.11-7 +# Absolutely require this version or newer Requires: libvirt-python >= 0.1.1 +# Definitely does not work with earlier due to python API changes Requires: dbus-python >= 0.61 +# Might work with earlier, but this is what we've tested +# We use 'ctypes' so don't need the 'gnome-keyring-python' bits +Requires: gnome-keyring >= 0.4.9 +# Minimum we've tested with +Requires: python-ctypes >= 0.9.9.6 # src/vncViewer/image.py needs either this Requires: python-imaging