[sabayon] Initial group dialog work
- From: Federico Mena Quintero <federico src gnome org>
- To: svn-commits-list gnome org
- Cc:
- Subject: [sabayon] Initial group dialog work
- Date: Mon, 26 Oct 2009 20:49:19 +0000 (UTC)
commit cdb647e8e9d264dad1c1199e3290df3a7cab25ee
Author: Scott Balneaves <sbalneav ltsp org>
Date: Tue Sep 1 22:41:42 2009 -0500
Initial group dialog work
admin-tool/groupsdialog.py | 121 +++++++++++
lib/groupdb.py | 486 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 607 insertions(+), 0 deletions(-)
---
diff --git a/admin-tool/groupsdialog.py b/admin-tool/groupsdialog.py
new file mode 100644
index 0000000..ab3f524
--- /dev/null
+++ b/admin-tool/groupsdialog.py
@@ -0,0 +1,121 @@
+#
+# Copyright (C) 2005 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+
+import os.path
+import pwd
+import gtk
+import gtk.glade
+import groupdb
+import errors
+import debuglog
+
+from config import *
+
+class GroupsModel (gtk.ListStore):
+ (
+ COLUMN_GROUP,
+ COLUMN_APPLY
+ ) = range (2)
+
+ def __init__ (self, db, profile):
+ gtk.ListStore.__init__ (self, str, str, bool)
+ for group in db.get_groups ():
+
+ row = self.append ()
+ self.set (row,
+ self.COLUMN_GROUP, group,
+ self.COLUMN_APPLY, profile == db.get_profile (group, False, True))
+
+class GroupsDialog:
+ def __init__ (self, profile, parent):
+ self.profile = profile
+ self.groupdb = groupdb.get_database ()
+
+ apply_to_all = self.groupdb.get_default_profile (False) == profile
+
+ glade_file = os.path.join (GLADEDIR, "sabayon.glade")
+ self.xml = gtk.glade.XML (glade_file, "groups_dialog", PACKAGE)
+
+ self.dialog = self.xml.get_widget ("groups_dialog")
+ self.dialog.set_transient_for (parent)
+ self.dialog.set_default_response (gtk.RESPONSE_CLOSE)
+ self.dialog.set_icon_name ("sabayon")
+ self.dialog.set_title (_("groups for profile %s")%profile)
+
+ self.close_button = self.xml.get_widget ("groups_close_button")
+
+ self.help_button = self.xml.get_widget ("groups_help_button")
+ self.help_button.hide ()
+
+ self.groups_model = GroupsModel (self.groupdb, self.profile)
+
+ self.groups_list_scroll = self.xml.get_widget ("groups_list_scroll")
+ self.groups_list = self.xml.get_widget ("groups_list")
+ self.groups_list.set_model (self.groups_model)
+ self.groups_list.set_sensitive (not apply_to_all)
+
+ c = gtk.TreeViewColumn (_("Group"),
+ gtk.CellRendererText (),
+ text = GroupsModel.COLUMN_GROUP)
+ c.set_sort_column_id(GroupsModel.COLUMN_GROUP)
+ self.groups_list.append_column (c)
+ self.groups_model.set_sort_column_id(GroupsModel.COLUMN_GROUP, gtk.SORT_ASCENDING)
+
+
+ toggle = gtk.CellRendererToggle ()
+ toggle.connect ("toggled", self.__on_use_toggled)
+ c = gtk.TreeViewColumn (_("Use This Profile"))
+ c.pack_start (toggle, False)
+ c.set_attributes (toggle, active = GroupsModel.COLUMN_APPLY)
+ self.groups_list.append_column (c)
+
+ response = self.dialog.run ()
+ self.dialog.hide ()
+
+ @errors.checked_callback (debuglog.DEBUG_LOG_DOMAIN_USER)
+ def __on_use_toggled (self, toggle, path):
+ iter = self.groups_model.get_iter_from_string (path)
+ apply = self.groups_model.get_value (iter, GroupsModel.COLUMN_APPLY)
+
+ apply = not apply
+
+ self.groups_model.set (iter, GroupsModel.COLUMN_APPLY, apply)
+
+ groupname = self.groups_model.get_value (iter, GroupsModel.COLUMN_GROUP)
+
+ if apply:
+ self.groupdb.set_profile (groupname, self.profile)
+ else:
+ self.groupdb.set_profile (groupname, None)
+
+ @errors.checked_callback (debuglog.DEBUG_LOG_DOMAIN_USER)
+ def __all_check_toggled (self, toggle):
+ apply_to_all = self.all_check.get_active ()
+ self.groups_list.set_sensitive (not apply_to_all)
+
+ if apply_to_all:
+ self.groupdb.set_default_profile (self.profile)
+ else:
+ self.groupdb.set_default_profile (None)
+
+if __name__ == "__main__":
+ import util
+
+ util.init_gettext ()
+
+ d = GroupsDialog ("foo", None)
diff --git a/lib/groupdb.py b/lib/groupdb.py
new file mode 100644
index 0000000..8782e14
--- /dev/null
+++ b/lib/groupdb.py
@@ -0,0 +1,486 @@
+#
+# Copyright (C) 2005 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+
+import sys
+import string
+import grp
+import os
+import libxml2
+import config
+import util
+import cache
+import random
+import ldap
+import socket
+import debuglog
+
+defaultConf="""<profiles>
+ <default profile=""/>
+</profiles>"""
+
+# make sure to initialize the cache first
+# this will make sure we can handle disconnection
+# and initialize libxml2 environment
+cache.initialize()
+
+def dprint (fmt, *args):
+ debuglog.debug_log (False, debuglog.DEBUG_LOG_DOMAIN_USER_DB, fmt % args)
+
+def get_setting (node, setting, default = None, convert_to = str):
+ a = node.hasProp(setting)
+ if a:
+ try:
+ return convert_to (a.content)
+ except:
+ np = node.nodePath()
+ # Translators: You may move the "%(setting)s" and "%(np)s" items as you wish, but
+ # do not change the way they are written. The intended string is
+ # something like "invalid type for setting blah in /ldap/path/to/blah"
+ raise GroupDatabaseException(_("invalid type for setting %(setting)s in %(np)s") % { "setting": setting,
+ "np": np })
+ return default
+
+def expand_string (string, attrs):
+ res = ""
+ i = 0
+ while i < len(string):
+ c = string[i]
+ i = i + 1
+ if c == "%":
+ if i < len(string):
+ c = string[i]
+ if c != "%":
+ if c in attrs:
+ c = attrs[c]
+ i = i + 1
+ res = res + c
+ return res
+
+
+class GroupDatabaseException (Exception):
+ pass
+
+class GroupDatabase:
+ """An encapsulation of the database which maintains an
+ association between groups and profiles.
+
+ This database is stored by default in
+ $(sysconfdir)/desktop-profiles/groups.xml and contains a
+ list of groups and the profile associated with each of
+ those groups.
+
+ A profile can be reference by either its name (in which case
+ the profile is stored at /etc/desktop-profiles/$(name).zip),
+ an absolute path or a http/file URL.
+ """
+ def __init__ (self, db_file = None):
+ """Create a GroupDatabase object.
+
+ @db_file: an (optional) path which specifes the location
+ of the database file. If not specified, the default
+ location of /etc/desktop-profiles/groups.xml is used.
+ """
+ if db_file is None:
+ file = os.path.join (config.PROFILESDIR, "groups.xml")
+ elif db_file[0] != '/':
+ file = os.path.join (config.PROFILESDIR, db_file)
+ else:
+ file = db_file
+ self.file = file;
+ self.modified = 0
+ dprint("New GroupDatabase(%s) object\n" % self.file)
+
+ try:
+ self.doc = libxml2.readFile(file, None, libxml2.XML_PARSE_NOBLANKS)
+ # Process XInclude statements
+ self.doc.xincludeProcess()
+ except:
+ # TODO add fallback to last good database
+ dprint("failed to parse %s falling back to default conf\n" %
+ self.file)
+ self.doc = None
+ if self.doc == None:
+ self.doc = libxml2.readMemory(defaultConf, len(defaultConf),
+ None, None,
+ libxml2.XML_PARSE_NOBLANKS);
+
+ def __del__ (self):
+ if self.doc != None:
+ self.doc.freeDoc()
+
+ def __profile_name_to_location (self, profile, node):
+ if not profile:
+ return None
+
+ uri = self.__ldap_query ("locationmap", {"p":profile, "h":socket.getfqdn()})
+ if uri:
+ return uri
+
+ # do the necessary URI escaping of the profile name if needed
+ orig_profile = profile
+ try:
+ tmp = parseURI(profile)
+ except:
+ profile = libxml2.URIEscapeStr(profile, "/:")
+
+ # if there is a base on the node, then use
+ if node != None:
+ try:
+ base = node.getBase(None)
+ if base != None and base != "" and \
+ base != os.path.join (config.PROFILESDIR, "groups.xml"):
+ # URI composition from the base
+ ret = libxml2.buildURI(profile, base)
+ if ret[0] == '/':
+ ret = libxml2.URIUnescapeString(ret, len(ret), None)
+ dprint("Converted profile name '%s' to location '%s'\n",
+ orig_profile, ret)
+ return ret
+ except:
+ pass
+ try:
+ uri = libxml2.parseURI(profile);
+ if uri.scheme() is None:
+ # it is a file path
+ if profile[0] != '/':
+ profile = os.path.join (config.PROFILESDIR, profile)
+ if profile[-4:] != ".zip":
+ profile = profile + ".zip"
+ else:
+ # TODO need to make a local copy or use the local copy
+ profile = profile
+ except:
+ # we really expect an URI there
+ profile = None
+
+ if profile[0] == '/':
+ profile = libxml2.URIUnescapeString(profile, len(profile), None)
+ dprint("Converted profile name '%s' to location '%s'\n",
+ orig_profile, profile)
+ return profile
+
+ def __open_ldap (self):
+ nodes = self.doc.xpathEval ("/profiles/ldap")
+ if len (nodes) == 0:
+ return None
+ ldap_node = nodes[0]
+
+ server = get_setting (ldap_node, "server", "localhost")
+ port = get_setting (ldap_node, "port", ldap.PORT, int)
+
+ l = ldap.open (server, port)
+
+ l.protocol_version = get_setting (ldap_node, "version", ldap.VERSION3, int)
+ l.timeout = get_setting (ldap_node, "timeout", 10, int)
+
+ bind_dn = get_setting (ldap_node, "bind_dn", "")
+ bind_pw = get_setting (ldap_node, "bind_pw", "")
+ if bind_dn != "":
+ l.simple_bind (bind_dn, bind_pw)
+
+ return l
+
+ def __ldap_query (self, map, replace):
+ nodes = self.doc.xpathEval ("/profiles/ldap/" + map)
+ if len (nodes) == 0:
+ return None
+ map_node = nodes[0]
+
+ l = self.__open_ldap ()
+ if not l:
+ return None
+
+ search_base = get_setting (map_node, "search_base")
+ query_filter = get_setting (map_node, "query_filter")
+ result_attribute = get_setting (map_node, "result_attribute")
+ scope = get_setting (map_node, "scope", "sub")
+ multiple_result = get_setting (map_node, "multiple_result", "first")
+
+ if search_base == None:
+ raise GroupDatabaseException(_("No search base specified for %s"%map))
+
+ if query_filter == None:
+ raise GroupDatabaseException(_("No query filter specified for %s"%map))
+
+ if result_attribute == None:
+ raise GroupDatabaseException(_("No result attribute specified for %s"%map))
+
+ if scope == "sub":
+ scope = ldap.SCOPE_SUBTREE
+ elif scope == "base":
+ scope = ldap.SCOPE_BASE
+ elif scope == "one":
+ scope = ldap.SCOPE_ONELEVEL
+ else:
+ raise GroupDatabaseException(_("Scope must be one of sub, base and one"))
+
+ query_filter = expand_string (query_filter, replace)
+ search_base = expand_string (search_base, replace)
+
+ results = l.search_s (search_base, scope, query_filter, [result_attribute])
+
+ if len (results) == 0:
+ return None
+
+ (dn, attrs) = results[0]
+ if not result_attribute in attrs:
+ return None
+ vals = attrs[result_attribute]
+
+ if multiple_result == "first":
+ val = vals[0]
+ elif multiple_result == "random":
+ val = vals[random.randint(0, len(vals)-1)]
+ else:
+ raise GroupDatabaseException(_("multiple_result must be one of first and random"))
+
+ l.unbind ()
+
+ return val
+
+
+ def get_default_profile (self, profile_location = True):
+ """Look up the default profile.
+
+ @profile_location: whether the profile location should
+ be returned
+
+ Return value: the location of the default profile, which
+ should be in a suitable form for constructing a ProfileStorage
+ object, or the default profile name if @profile_location is
+ False.
+ """
+ default = None
+ try:
+ default = self.doc.xpathEval("/profiles/default")[0]
+ profile = default.prop("profile")
+ except:
+ profile = None
+
+ if not profile_location:
+ return profile
+
+ return self.__profile_name_to_location (profile, default)
+
+ def get_profile (self, groupname, profile_location = True, ignore_default = False):
+ """Look up the profile for a given groupname.
+
+ @groupname: the group whose profile location should be
+ returned.
+ @profile_location: whether the profile location should
+ be returned
+ @ignore_default: don't use the default profile if
+ no profile is explicitly set.
+
+ Return value: the location of the profile, which
+ should be in a suitable form for constructing a
+ ProfileStorage object, or the profile name if
+ @profile_location is False.
+ """
+ group = None
+ profile = self.__ldap_query ("profilemap", {"g":groupname, "h":socket.getfqdn()})
+ if not profile:
+ try:
+ query = "/profiles/group[ name='%s']" % groupname
+ group = self.doc.xpathEval(query)[0]
+ profile = group.prop("profile")
+ except:
+ profile = None
+ if not profile and not ignore_default:
+ try:
+ query = "/profiles/default[1][ profile]"
+ group = self.doc.xpathEval(query)[0]
+ profile = group.prop("profile")
+ except:
+ profile = None
+
+ if not profile_location:
+ return profile
+
+ # TODO Check the resulting file path exists
+ return self.__profile_name_to_location (profile, group)
+
+ def __save_as(self, filename = None):
+ """Save the current version to the given filename"""
+ if filename == None:
+ filename = self.file
+
+ dprint("Saving GroupDatabase to %s\n", filename)
+ try:
+ os.rename(filename, filename + ".bak")
+ backup = 1
+ except:
+ backup = 0
+ pass
+
+ try:
+ f = open(filename, 'w')
+ except:
+ if backup == 1:
+ try:
+ os.rename(filename + ".bak", filename)
+ dprint("Restore from %s.bak\n", filename)
+ except:
+ dprint("Failed to restore from %s.bak\n", filename)
+
+ raise GroupDatabaseException(
+ _("Could not open %s for writing") % filename)
+ try:
+ f.write(self.doc.serialize("UTF-8", format=1))
+ f.close()
+ except:
+ if backup == 1:
+ try:
+ os.rename(filename + ".bak", filename)
+ dprint("Restore from %s.bak\n", filename)
+ except:
+ dprint("Failed to restore from %s.bak\n", filename)
+
+ raise GroupDatabaseException(
+ _("Failed to save GroupDatabase to %s") % filename)
+
+ self.modified = 0
+
+ def set_profile (self, groupname, profile):
+ """Set the profile for a given groupname.
+
+ @groupname: the group whose profile location should be
+ set.
+ @profile: the location of the profile.
+ """
+ if profile is None:
+ profile = ""
+ self.modified = 0
+ try:
+ query = "/profiles/group[ name='%s']" % groupname
+ group = self.doc.xpathEval(query)[0]
+ oldprofile = group.prop("profile")
+ if oldprofile != profile:
+ group.setProp("profile", profile)
+ self.modified = 1
+ except:
+ try:
+ profiles = self.doc.xpathEval("/profiles")[0]
+ except:
+ raise GroupDatabaseException(
+ _("File %s is not a profile configuration") %
+ (self.file))
+ try:
+ group = profiles.newChild(None, "group", None)
+ group.setProp("name", groupname)
+ group.setProp("profile", profile)
+ except:
+ raise GroupDatabaseException(
+ _("Failed to add group %s to profile configuration") %
+ (groupname))
+ self.modified = 1
+ if self.modified == 1:
+ self.__save_as()
+
+ def get_profiles (self):
+ """Return the list of currently available profiles.
+ This is basically just list of zip files in
+ /etc/desktop-profiles, each without the .zip extension.
+ """
+ list = []
+ try:
+ for file in os.listdir(config.PROFILESDIR):
+ if file[-4:] != ".zip":
+ continue
+ list.append(file[0:-4])
+ except:
+ dprint("Failed to read directory(%s)\n" % (config.PROFILESDIR))
+ # TODO: also list remote profiles as found in self.doc
+ return list
+
+ def is_sabayon_controlled (self, groupname):
+ """Return True if groups's configuration was ever under Sabayon's
+ control.
+ """
+ profile = self.__ldap_query ("profilemap", {"g":groupname, "h":socket.getfqdn()})
+
+ if profile:
+ return True
+
+ try:
+ query = "/profiles/group[ name='%s']" % groupname
+ group = self.doc.xpathEval(query)[0]
+ except:
+ return False
+
+ if group:
+ return True
+
+ return False
+
+ def get_groups (self):
+ """Return the list of groups on the system. These should
+ be real groups - i.e. should not include system groups
+ like nobody, gdm, nfsnobody etc.
+ """
+ list = []
+ try:
+ groups = grp.getgrall()
+ except:
+ raise GroupDatabaseException(_("Failed to get the group list"))
+
+ for group in groups():
+ try:
+ # remove non-groups
+ if group[2] < 500:
+ continue
+ if group[0] in list:
+ continue
+ list.append(group[0])
+ except:
+ pass
+ return list
+
+
+
+group_database = None
+def get_database ():
+ """Return a GroupDatabase singleton"""
+ global group_database
+ if group_database is None:
+ group_database = GroupDatabase ()
+ return group_database
+
+#
+# Unit tests
+#
+def run_unit_tests ():
+ testfile = "/tmp/test_groups.xml"
+ try:
+ os.unlink(testfile)
+ except:
+ pass
+ db = GroupDatabase(testfile)
+ res = db.get_profile("localgroup", False)
+ db.set_profile("localgroup", "groupA")
+ res = db.get_profile("localgroup")
+ assert not res is None
+ assert res[-28:] == "/desktop-profiles/groupA.zip"
+ db.set_profile("localgroup", "groupB")
+ res = db.get_profile("localgroup")
+ assert not res is None
+ assert res[-28:] == "/desktop-profiles/groupB.zip"
+
+if __name__ == "__main__":
+ util.init_gettext ()
+ run_unit_tests()
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]