- Error logging
- Exceptions in callbacks
- Configuration sources, changes, and delegates
- Applying Profiles
- User and Group databases
- LDAP profile storage
Sabayon's code is split among three processes: the sysadmin-level application, and two internal helpers. The main administration tool is the sabayon process: it runs as root, and it is the little window that shows you the list of user profiles. It uses two helper processes. The first helper is sabayon-session, which runs as a temporary user and shows the live Xephyr session. The second helper is sabayon-apply, which is used to apply the settings from the user profile being edited into a temporary home directory.
sabayon - main tool used by admins
sabayon-session - helper to run the nested session in a window
sabayon-apply - system tool and helper to apply a user profile to a home directory
Whitelist of environment variables
Sabayon sanitizes the environment before launching its child processes, so that extraneous environment variables don't sneak in. There is a whitelist of environment variables that will be passed through to the child processes in sabayon/lib/config.py.in:PASSTHROUGH_ENVIRONMENT. If you need to pass an environment variable to the child processes or to the child session, you'll need to modify PASSTHROUGH_ENVIRONMENT.
The helper processes must be able to keep detailed logs of their operation to aid debugging. These logs must be accessible from the main "sabayon" tool which admins use: when the tool encounters an error in itself or in any of its helpers, it must be able to give the admin useful information for debugging (either by the admin himself or by a Sabayon hacker).
While each helper process could maintain its own log, this would be cumbersome for admins to gather and send to a developer. So, Sabayon automatically aggregates all the logs into a single one which can be sent "as-is" to someone for debugging purposes.
Turning on logging
By default, all user actions are logged. That is, Sabayon and its helpers keep logs of the actions which the admin does while directly using them. Sabayon will automatically present these logs to the admin when an error occurs.
Additionally, the admin can enable more detailed logging. Logging is controlled by the sabayon-debug-log.conf file. This file has a syntax similar to Samba's smb.conf or to Windows .ini files. Put the following text in the /etc/sabayon/sabayon-debug-log.conf file:
[debug log] max lines = 1000 enable domains = <list of domain names separated by ";">
For example, to enable all logging, you would use a line like this:
enable domains = user-profile;storage;panel-delegate;gconf-source;files-source;mozilla-source;proto-session;usermod;dir-monitor;user-db;cache;admin-tool;sabayon-apply;sabayon-session
Sabayon will create a file in root's home directory with the logging information. The format of the file will be sabayon-debug-log-<date><time>.txt.
These are the available domain names. Note that by default they are not logged unless you list them in the sabayon-debug-log.conf file:
user-profile - Main module to handle user profiles.
storage - Module to save user profiles to a .zip bundle.
panel-delegate - Understands changes which affect the GNOME Panel and its applets.
gconf-source - Monitoring changes in GConf data and logging/saving/restoring them.
files-source - Monitoring of changes in the temporary home directory and logging/saving/restoring them.
mozilla-source - Understands changes in the configuration of Mozilla Firefox.
proto-session - Low-level setup of the nested session (X authority, X displays, etc.).
usermod - Low-level utilities to create a temporary home directory.
dir-monitor - Low-level monitoring of the file system.
user-db - Maintains an association between users and profiles.
cache - Retrieves and caches the contents of remote URIs.
admin-tool - Toplevel operations in the sabayon program (not normally needed).
sabayon-apply - Toplevel operations in the sabayon-apply helper program (not normally needed).
sabayon-session - Toplevel operations in the sabayon-session helper program (not normally needed).
deprecated - Turns on warnings about using deprecated Python or pygtk features (not normally needed).
Exceptions in callbacks
Often, the code flow in Sabayon looks like this:
python code: (1) gtk_main (): (2) python code inside a callback: (3) ... (4)
If the code inside the callback (3) throws an exception in (4), then Python will go to (2) and the main loop will keep spinning happily. That is, (1) will never know about that exception! It will seem as if the program kept running normally.
If a callback gets an exception due to a bug, we want the toplevel program to notice that as soon as possible so that it can log the appropriate message to the debug log, and terminate the program. So, we use the errors.checked_callback decorator. This decorator catches all unhandled exceptions in a callback, logs the exception to the debug log, and calls gtk.main_quit(). You must use the decorator in all GObject or GSource callbacks (this includes idle handlers, GIO watches, etc.):
import errors @errors.checked_callback def my_callback (...): ... body ...
And finally, the toplevel program (or whatever calls gtk.main()) should look like this:
import errors gtk.main () if errors.errors_have_fatal_error (): debuglog.debug_log (True, "domain", "a fatal error occurred; exiting the program") debuglog.debug_log_dump_to_dated_file (...) sys.exit (1)
Configuration sources, changes, and delegates
Sabayon monitors different types of data sources. For example, it monitors which files are created in the session's home directory, or which GConf keys are modified.
The core of Sabayon doesn't know about particular data sources. There is an abstract class, ProfileSource in lib/userprofile.py, which all source types must implement. All the sources we have are in the lib/sources directory. The two major sources are FilesSource and GConfSource. Respectively, they monitor changes in files and changes in the GConf keys.
Each ProfileSource basically monitors some kind of data and creates a ProfileChange object every time something happens in that data. ProfileChange, in lib/userprofile.py, is an abstract class for changes in the user's configuration data.
Each major ProfileSource defines its own kind of ProfileChange. For example, FilesSource generates FileChange objects, and GConfSource generates GConfChange objects. A FileChange has information like "$filename got created/deleted/modified". A GConfChange has information like "$gconf_key_name changed to $value".
The major sources are useful for most configuration data. However, sometimes Sabayon would like to present a higher-level view of changes than just "this file got modified" or "that GConf key changed". For example, Firefox tweaks many files under ~/.mozilla, and gnome-panel tweaks many GConf keys under the /apps/panel namespace.
For each type of ProfileSource, we can specialize the way in which changes are handled through the use of a delegate.
For example, the MozillaDelegate (in lib/sources/mozillasource.py) says that every change detected by FilesSource, which is also under .mozilla, should be handled by the delegate instead of the generic code. MozillaDelegate has special knowledge about how to parse Firefox's preferences, bookmarks, etc.
Similarly, PanelDelegate (in lib/sources/paneldelegate.py) knows how to understand changes under the /apps/panel namespace in GConf. The panel sets a bunch of GConf keys when an applet is added. Instead of presenting the user with a meaningless list of GConf changes ("OAFIID for the applet, panel toplevel ID, blah blah"), the PanelDelegate parses that and instead presents human-readable information like "Workspace Switcher Applet got added to the bottom panel".
Each kind of delegate gets triggered by a a namespace section from the configuration space which its parent ProfileSource is monitoring. The namespace section of MozillaDelegate is thus ".mozilla", relative to the FilesSource which monitors the session's home directory, as that is the directory under ~ which it wants to "own". Similarly, the namespace section of PanelDelegate is "/apps/panel", relative to the GConfSource.
Packagers of Sabayon for distributions should ensure the following lines are added to /etc/gconf/2/path, or wherever their distribution handles gconf's path file:
include "$(HOME)/.gconf.path.mandatory" include "$(HOME)/.gconf.path.defaults"
Sabayon will create these files as part of the profile, which will point to the mandatory and default settings that Sabayon will create.
You'll also want some kind of Xsession script which will apply the profile as part of the login process. An example script would be:
# # Apply the Sabayon profile for the current user (if any) # if [ "x$DISABLE_SABAYON_XINITRC" = "x" ] ; then if [ -x /usr/sbin/sabayon-apply ] ; then /usr/sbin/sabayon-apply fi fi
If you want to apply scripts manually by group (this is supported by Sabayon in the gui), you can use a script like the following:
# #Script : apply a profile to a group of users #Argument 1 : name of the profile #Argument 2 : name of the group # chemin_profil="/etc/sabayon/profiles/$1".zip if [ $# -eq 2 ] then if [ -a $chemin_profil ] then getent group | cut -d ":" -f 1 | grep $2 > /dev/null result=$? if [ $result -eq 0 ] then if [ -x /usr/sbin/sabayon-apply ] then groupe=`getent group | cut -d ":" -f 1,3 | grep $2 | cut -d ":" -f 2` getent passwd | while read ligne do gid=$(echo $ligne | cut -d ":" -f 4) name=$(echo $ligne | cut -d ":" -f 1) if [ $gid -eq $groupe ] then home=$(getent passwd | cut -d ":" -f 1,6 | grep $name | cut -d ":" -f 2) touch $home/.gconf.path touch $home/.gconf.path.defaults touch $home/.gconf.path.mandatory echo "include $home/.gconf.path.defaults" > $home/.gconf.path echo "include $home/.gconf.path.mandatory" >> $home/.gconf.path echo xml:readonly:$home/.gconf.xml.defaults > $home/.gconf.path.defaults echo xml:readonly:$home/.gconf.xml.mandatory > $home/.gconf.path.mandatory if [ -a $home/.gconf.xml.defaults ] then rm -r $home/.gconf.xml.defaults fi if [ -a $home/.gconf.xml.mandatory ] then rm -r $home/.gconf.xml.mandatory fi su $name -c "/usr/sbin/sabayon-apply $chemin_profil" chown -R root $home/.gconf.path chgrp -R root $home/.gconf.path chmod 755 $home/.gconf.path chown -R root $home/.gconf.path.defaults chgrp -R root $home/.gconf.path.defaults chmod 755 $home/.gconf.path.defaults chown -R root $home/.gconf.path.mandatory chgrp -R root $home/.gconf.path.mandatory chmod 755 $home/.gconf.path.mandatory chown -R root $home/.gconf.xml.defaults chgrp -R root $home/.gconf.xml.defaults chmod 755 $home/.gconf.xml.defaults chown -R root $home/.gconf.xml.mandatory chgrp -R root $home/.gconf.xml.mandatory chmod 755 $home/.gconf.xml.mandatory echo "The profile $1 has been applied to $name." fi done echo "The profile $1 has been applied to the group $2." fi else echo "The group does not exist." fi else echo "The profile does not exist." fi else echo "" echo "Usage: $0 [name of the profile] [name of the group]" echo "" fi
User and Group databases
The user configuration database file is located in /etc/sabayon/users.xml (see PROFILESDIR in the config.py file to change the prefix), It is an XML file and has the following format:
<profiles> <user name="titeuf" profile="developer"/> <user name="fab" profile="developer"/> <user name="bianca" profile="secretary"/> ... <default profile="default"/> </profiles>
It contains a list of named users, and their associated profile. The profile in that case are aliases, for example developer is a shortcut for developer.zip in the same directory.
The default profile will be applied for any login not listed explicitely in the set of users.
The group configuration database file is located in /etc/sabayon/groups.xml (see PROFILESDIR in the config.py file to change the prefix), It is an XML file and is similar to the user database:
<profiles> <group name="grade1" profile="Grade1"/> <group name="grade2" profile="Grade1"/> </profiles>
Users who are members of the group specified by name will have the profile applied to them.
== Centralized user databases and profiles
For large and automated deployement, maintaining copies of the user and group databases and profiles on all machines does not really make sense. To avoid the problem, the profile values can be an URI - preferably using HTTP - to reference a remote resource like:
... <user name="titeuf" profile="http://server.corp.com/prof/developers.zip"/> <user name="fab" profile="http://server.corp.com/prof/developers.zip"/> ...
The profiles should then be maintained on a separate machine and copied over to the server when updated.
The user database can also be centralized, using the XInclude mechanism for XML. For example the following assume there is a developer list maintained separately on the server, and defining the profiles for them:
<profiles> <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="http://server.corp.com/user/devel.xml#xpointer(//user)"/> ... <default profile="default"/> </profiles>
This XInclude will just collect all user definitions in the developer list at http://server.corp.com/user/devel.xml and replace the include statement with that list, and process the XML as usual. The only difference is that the profile values are then assumed to be URI-References and for example if the devel.xml contains
... <user name="titeuf" profile="../prof/developers.zip"/> ...
then Sabayon will fetch the profile relative to the location of devel.xml, that is it will lookup the profiles relative to the base of where the fragment was defined and use http://server.corp.com/prof/developers.zip. Also note that shortcut are not allowed there, the trailing .zip is needed.
The last point about the centralized support is that Sabayon will use a cache located in the user home directory under .sabayon/profile_cache to keep a copy of the non-local files, this allows profile to function normally in disconnected operations or in case of server failures.
Format of profile storage
The following is a description of the requirement and the choices made when designing the user profile files. The existing format may change in the future but unless some of the requirement were missing it seems the existing choice is simple and flexible enough that no big change should be needed in the future.
- incremental update
- container for sets of settings of different apps
- associate apps with settings
- independent update of one set of settings
- ability to store full file
- ability to save path with the content
- provide metadata for the whole set and for each apps settings
- possibility to merge and detect potential clashes on merges
- possibility to extract or remove a simple set of data
- allow to process with as standard tools as possible
Use ZIP for the container format:
- platform ubiquity Linux/Windows/Mac...
- free software tools and libraries
- allows to access a single stream without exploding everything like a compressed tar or cpio requires
- allow to store name/paths
XML Metadata description
Add an XML metadata section as the first entry:
- classic design (jar)
- allow to store all metadata informations associated
- easy to extend in a backward and forward compatible way
- open source tools and local knowledge
- a lot of configuration data already require XML handling so this doesn't add an extra dependancy
- load/modify subpart/save operations are easy on an XML tree
The container is a Zip, its content can be viewed using the command
unzip -l /etc/sabayon/profiles/test.zip
The first entry is the metadata part, it is an XML file describing the content of the archive. It can be viewed using the command
unzip -p /etc/sabayon/profiles/test.zip metadata
Currently, the metadata contains
- general description for the whole set of settings e.g. "Configuration for developers in project foo":
- administrative informations
- last change timestamp
- per stream description
- name of the stream in the archive
- associated application
- description for settings e.g. "Mozilla starts full screen"
- administrative info
Then in the ZIP, each update has its own stream, the format is left to the corresponding user profile writer module. It can potentially be a full raw file, or a more synthetic description recognized by the specific profile module.
Note that an application can have one stream per different configuration file for example .openoffice1.1/user/registry/data/org/openoffice/Inet.xcu and .openoffice1.1/user/registry/data/org/openoffice/Office/Common.xcu would be 2 different streams maintained by the OpenOffice reader writer module. An application may have both raw configuration files and digested name/values pairs, but in different files.
LDAP profile storage
Sabayon supports using LDAP to get profiles in a very flexible way. By defining server settings and queries in the /etc/sabayon/users.xml file it can do the mapping from user to profile file using LDAP queries. An example setup can look like:
<profiles> <ldap server="ldap.example.com"> <profilemap search_base="ou=People,dc=example,dc=com" scope="one" query_filter="(uid=%u)" result_attribute="sabayonProfileURL"/> </ldap> <default profile="default"/> </profiles>
Note: currently in Sabayon's git HEAD, this has changed. Servers are now specified using the "uri" parameter. Multiple servers can be specified by comma separation. So, for example:
<profiles> <ldap uri="ldap://ldap1.example.com,ldap://ldap2.example.com"> <profilemap search_base="ou=People,dc=example,dc=com" scope="one" query_filter="(uid=%u)" result_attribute="sabayonProfileURL"/> </ldap> <default profile="default"/> </profiles>
The uri's will be tried in order.
LDAP server configuration
The toplevel ldap tag sets up the server connection. Availible attributes are:
- server (default: localhost): The address of the ldap server
- port (default: 389): The port of the ldap server
uri (default: ldap://localhost): RFC 4516 ldap uri specifier (git HEAD, replaces server & port)
- version (default: 3): The ldap version to use
- timeout (default: 10): Timeout to use for ldap operations, 0 to disable
- bind_dn: dn to bind as, or leave out to run queries without binding
- bind_pw: password used when binding
Inside the ldap tag you can define the two queries used by Sabayon. The first is the profilemap query, which maps from the username to the profile name to use for the user. The profile name is just a string, and it can be either an absolute URI, a URI relative to the config file, or just a name. If it is a name it will be looked up in the locationmap LDAP query (if specified) and if that didn't match, it will be converted to a filename in /etc/sabayon/profiles by appending ".zip".
The locationmap query specifies the mapping from profile name to profile URI. This can optinally be used instead of storing the profile URI directly in the LDAP user object, to allow more flexibility in changing the URI.
Both queries support these attributes:
- search_base: The search base of the query
- query_filter: the LDAP filter to use
- result_attribute: The name of the attribute to look at in the query result
- scope (default "sub"): The search scope. Can be sub, base or one.
- multiple_result (default "first"): How to handle multiple values in the resulting attribute. Can be "first", use the first attribute or "random", pick a random one (for e.g. load balancing).
Both search_base and query_filter are expanded. In profilemap %u expands to the username and in locationmap %p expands to the profile name. In both maps %h expands to the full hostname of the client, and %% expands to %.
There are many way to set up the Sabayon LDAP integration. Here are some examples, all using the Sabayon LDAP schema that comes in the Sabayon package.
Store profile URL in the user
This is the simplest setup. Add a sabayonProfileURLObject to your user objects and set the sabayonProfileURL property for each user to a URI of the profile.
... <ldap server="..."> <profilemap search_base="ou=People,dc=example,dc=com" scope="one" query_filter="(uid=%u)" result_attribute="sabayonProfileURL"/> </ldap> ...
Store profile as a separate entity in ldap
Store the name of the profile for each user (using a sabayonProfileNameObject object), and store the actual URI in a sabayonProfile object. This gives a lot of flexibility to change the URI of the profile, without having to update each user.
... <ldap server="..."> <profilemap search_base="ou=People,dc=example,dc=com" scope="one" query_filter="(uid=%u)" result_attribute="sabayonProfileName"/> <locationmap search_base="ou=Profiles,dc=example,dc=com" scope="one" query_filter="(cn=%p)" result_attribute="sabayonProfileURL"/> </ldap> ...
Pick profile based on group membership
This lets you pick a profile based on what group the user is part of by adding the sabayonProfileURL attribute to the group object.
... <ldap server="..."> <profilemap search_base="ou=Group,dc=example,dc=com" scope="one" query_filter="(memberUid=%u)" result_attribute="sabayonProfileURL"/> </ldap> ...
There are countless other ways, for example you could combine the group example and the separate profile object example. If you come up with an interesting way, please tell us on the mailing list.