Debian Configlets

Jeff Licquia

Eric Gillespie

John R. Daily

$Progeny$


Table of Contents

1. Introduction
Overview
2. Architecture
Components
3. Writing a configlet
Files
Python code
Multi-Page Configlets
4. Front End Programming
Introduction
Writing a Front End
The BasicConfigGroup Class
Starting Configlets
Interacting with Debconf
A. API Reference

List of Examples

3.1. Etherconf gnome_setup
3.2. Etherconf attributes and registration
3.3. Etherconf load_debconf
3.4. Etherconf report_debconf

Chapter 1. Introduction

$Progeny$

Table of Contents

Overview

Overview

The debconf system, while very useful, is forced by design to provide a least common denominator approach to its user interface. While this is very useful for accomodating the many environments that a package might be configured, it does make debconf interfaces in richer environments look ugly and difficult to use by comparison.

Configlets supply an architecture to support richer front ends that can leverage advanced environments such as GNOME. Developers can create Python code and a Glade interface to be presented to the user by a separate front end; behind the scenes, debconf is used to store questions and answers for future use, just as it would otherwise.

The current GNOME front ends include a druid to invoke each configlet as a wizard, and individual capplets that are incorporated into the GNOME control center.

Debian packages

  • python-configlets contains the core infrastructure.

  • configlet-frontends provides the GNOME druid front end and capplet interface.

Chapter 2. Architecture

$Progeny$

Table of Contents

Components

Components

Package maintainers may provide main.py and main.glade files to take advantage of the configlet infrastructure; see the API documentation for further details.

/usr/sbin/update-configlets invokes scripts installed by any existing configlet front ends in order to register new configlets.

A GNOME configlet druid, /usr/sbin/configlet-druid, provides a front-end wizard for all installed configlets.

The configlet module is the only component which may communicate directly with debconf; it does so via debconf-communicate(1).

/usr/bin/configlet-capplet is symlinked to configlet-specific scripts that are invoked by the GNOME Control Center.

The important Python classes to be re-used by configlet-druid and code supplied by package maintainers is found in /usr/lib/python2.1/site-packages/configlet.py.

configlet.py contains the Configlet base class, which individual configlets inherit from, as well as a simple container class which implements basic functionality for dynamically loading and instantiating configlets installed as subdirectories of a common configlet directory.

/usr/bin/test-configlets is a simple configlet front-end.

Chapter 3. Writing a configlet

$Progeny$

Files

Configlets consist of several files, all contained in a common directory. The minimal files included in this directory are:

main.glade

the Glade XML file that describes the user interface for the configlet. Creating and editing Glade files is beyond the scope of this document. Refer to the Glade web page for more information.

main.py

the Python code that implements the "meat" of the configlet's code.

These are the only two files directly referenced by the infrastructure classes. The developer is free to include new files or subdirectories in addition to these two. For example, some configlets may include icons or other graphics.

The Glade file has two requirements:

  • It must contain widgets that correspond to the configlet pages that the configlet needs to display. Each of these widgets must have a name that will be used by the configlet API. Since these widgets will be inserted into a parent object created by the front ends, they should not be toplevel objects; they may be wrapped by a toplevel to allow them to be edited in Glade, as long as those toplevel objects can be discarded at runtime. As a special case, single-page configlets may name their page widget mainwidget; this will enable some automatic handling by the API.

  • The configlet page widgets must not be set visible by default; they are set visible as a result of the configlet module when appropriate.

Python code

The Python file contains the code for the configlet. It must follow these rules:

  • It must import configlet.

  • It must define a class derived from configlet.Configlet. It must provide a gnome_setup method which does any GUI-specific initialization. For example, Etherconf's configlet uses gnome_setup to retrieve the widgets it uses from the wtree, get a list of network interfaces, and the following bit of code for automatically connecting signal handlers.

    Example 3.1. Etherconf gnome_setup

    
            dict = {}
            global Etherconf
            for key in dir(Etherconf):
                dict[key] = getattr(self, key)
            self.wtree.signal_autoconnect(dict)
    
                
  • It must define a dictionary of attributes, which are used to initialize the configlet. At least the following attributes must be set:

    • api_version: This should be set to the configlet API version this configlet supports. For this version of the configlets, this should be set to the integer 2.

    • display_title: This should be a short descriptive name for the configlet. Examples of good names would include "X Configuration" or "Network"; some bad ones would include "Configure the X Server's Display Settings" (too long) or "etherconf" (too short and user-hostile).

    • description: This should be a longer description of the configlet.

    • packages: This must be a list of the packages the configlet configures.

    Additionally, the page_names attribute must be defined as a list of the names of the configlet page widgets. Only one exception is allowed: single-page configlets may omit this attribute if the name for its page widget is mainwidget.

    Multi-page configlets must also define a page_display_titles attribute. This should be a dictionary containing short descriptive titles for each page of the configlet.

    Optionally, the priority attribute may be defined. This should be a number between 1 and 100; 1 is considered to be the highest priority, and will be displayed first, most prominently, or however the front end chooses to interpret priority. This value is entirely advisory, and may not make sense in all front ends. If not set, the API will set it to the default value of 50.

    This list of attributes is not exhaustive; the configlet may decide to set other attributes for other uses. See the API Reference for details.

    This dictionary of attributes must be passed to configlet.register_configlet(classname, , attributes).

    Example 3.2. Etherconf attributes and registration

    
    _attrs = { "name": "etherconf",
               "display_title": _("Configure Network Interfaces"),
               "description": _("Select this option to configure the devices your system uses to access networks such as the Internet."),
               "packages": ["etherconf", "postfix"]
    }
    
    configlet.register_configlet(Etherconf, _attrs)
    
                

If your configlet is a front end to Debconf questions (most are), you will also want to define load_debconf and report_debconf methods. load_debconf receives as the list of all Debconf values, not just those pertaining to the packages this configlet configures. It is up to the configlet to determine which values it is interested in.

Example 3.3. Etherconf load_debconf


    def load_debconf(self, dcdata):
        self.mail_config = []

        for i in dcdata:
            (template, question, value) = re.split(r"\s+", i, 2)
            (package, varname) = re.split("/", question, 1)

            if package == "postfix":
                self.mail_config.append(i)

            if package != "etherconf":
                continue

            if value == '""' or value == "none":
                value = ""

            if varname == "INT-devices":
                # We don't need to read this one, but we do need to
                # set it.  See debconf() below.
                pass
            elif varname == "hostname":
                if value:
                    self.hostname_entry.set_text(value)
            elif varname == "domainname":
                if value:
                    self.domainname_entry.set_text(value)
            elif varname == "nameservers":
                if value:
                    self.nameservers_entry.set_text(value)

            else:
                # If a ValueError occurs, this is not a dhcp-p:eth0
                # type question (which is what we're after), so it
                # doesn't matter what it is, we're simply not
                # interested.
                try:
                    (varname, device) = re.split(":", varname, 1)
                except ValueError:
                    continue

                info = self.get_device_info(device)
                if varname == "configure":
                    if value == "true":
                        info.configure = TRUE
                    else:
                        info.configure = FALSE
                elif varname == "removable":
                    if value == "true":
                        info.removable = TRUE
                    else:
                        info.removable = FALSE
                elif varname == "dhcp-p":
                    if value == "true":
                        info.dhcp = TRUE
                    else:
                        info.dhcp = FALSE
                elif varname == "dhcphost":
                    if value:
                        info.dhcphost = value
                elif varname == "ipaddr":
                    if value:
                        info.ip = value
                elif varname == "netmask":
                    if value:
                        info.netmask = value
                elif varname == "gateway":
                    if value:
                        info.gateway = value

        self.setup_menu(self.devices)
        self.change_device(self.devices[0])

        

Example 3.4. Etherconf report_debconf


    def _dcstring(var, val, device=None):
        if device:
            s = "etherconf/%s etherconf/%s:%s %s" % (var, var, device, val)
        else:
            s = "etherconf/%s etherconf/%s %s" % (var, var, val)

        debug("Reporting %s" % (s,))
        return s

    def report_debconf(self):
        results = []
        int_devices = ""

        results.extend(self.mail_config)
        results.append("postfix/relayhost postfix/relayhost %s"
                       % (self.relayhost,))
        results.append("postfix/mailname postfix/mailname %s.%s"
                       % (self.hostname, self.domainname))

        results.append(_dcstring("replace-existing-files", "true"))
        results.append(_dcstring("hostname", self.hostname))
        results.append(_dcstring("domainname", self.domainname))
        results.append(_dcstring("nameservers", self.nameservers))

        for i in self.devices:
            int_devices = "%s:%s" % (int_devices, i)
            info = self.get_device_info(i)

            if info.configure:
                results.append(_dcstring("configure", "true", i))
            else:
                results.append(_dcstring("configure", "false", i))

            if info.removable:
                results.append(_dcstring("removable", "true", i))
            else:
                results.append(_dcstring("removable", "false", i))

            if info.dhcp:
                results.append(_dcstring("dhcp-p", "true", i))
            else:
                results.append(_dcstring("dhcp-p", "false", i))

            results.append(_dcstring("dhcphost", info.dhcphost, i))
            results.append(_dcstring("ipaddr", info.ip, i))
            results.append(_dcstring("netmask", info.netmask, i))
            results.append(_dcstring("gateway", info.gateway, i))

        results.append(_dcstring("INT-devices", int_devices[1:]))

        return results

        

Configlet directories must be located in a standard place, /usr/share/configlets/, where front ends can find them easily. Note that the directory name under /usr/share/configlets is arbitrary, but should be descriptive and unlikely to risk a collision with other configlets, and must not contain spaces or other unusual characters.

Here is a sample hierarchy.

 /usr/share/configlets
   |
   +--etherconf
   |   |
   |   +--main.glade
   |   +--main.py
   |
   +--timezoneconf
   |   |
   |   +--main.glade
   |   +--main.py
   |
 (and so on)
      

Any package providing a configlet must call update-configlets --install directory_name in its postinst and update-configlets --remove directory_name in its prerm; this allows the configlet system to register configlets with front ends that require registration of some kind. This will require that the package either test for /usr/sbin/update-configlets in the postinst or declare a dependency on the configlet package.

Multi-Page Configlets

For a standard configlet, this is all you need. For very complex configlets, however, you might want several pages of configuration screens; this shouldn't be done with a simple tabbed widget to prevent nested tab widgets (if the front end implements tabs, for example). Instead, implement a multi-page configlet. The front end can then present the pages in whichever way makes sense.

To implement a multipage configlet, create your multiple pages in Glade and save them in main.glade. For each page, the container widget that envelops the entire page should be named with a sensible name. Then, set the attribute page_names to a list of these names. Optionally, you may set the page_display_titles attribute as well; this should be a mapping between page names (as in the page_names attribute) and the short descriptive titles for each page.

You can also define validate_page that can be called by the front end to validate a particular page's input. As with validate, this function is advisory only; there are no guarantees that it will be called at all, that it will be called in any particular order, or that its results will not be ignored. It returns a similar value to validate, and takes the name of the page to validate as its only argument. The default definitions for validate and validate_page complement each other; thus, a configlet should only override one or the other.

Chapter 4. Front End Programming

$Progeny$

Introduction

The front end is the program that takes care of much of the housekeeping duties for the configlets. It is responsible for starting the configlets, providing them a GUI toplevel widget of some kind to live in, handling global user interaction (such as navigation between the configlets), reading from and writing to the debconf database, and so on.

Front ends generally distinguish themselves by the way they allow navigation between the configlets, as most of the rest of the front end's duties are rather mundane. For example, the front end that ships with the configlet API package places each configlet in a separate tab; other front ends exist that present the configlets in a GNOME Druid widget (similar to Microsoft's Wizards) or that present the configlets as items in the GNOME Control Center. For this reason, most of the housekeeping code is provided by the API itself, leaving the front end with little else except presentation and navigation functions.

Writing a Front End

Front ends generally perform a standard set of tasks in the same order.

  • Initialize and start the configlet(s) to be displayed.

  • Read the debconf database, and pass that data to the configlets.

  • Draw (or otherwise create) the top level widgets to contain the configlets, and insert the configlets into them.

  • Interact with the user. This includes handling of navigation events, OK/Cancel, or other possible events that could be generated by the user.

  • When the user is finished with the configlets, read the changed debconf information back from them and write it to the debconf database.

  • Shut down the configlets, destroy the user interface, and terminate.

The BasicConfigGroup Class

For many of these functions, the API provides helper functions that handle the most common cases. These functions are provided as methods to the BasicConfigGroup class. Most front ends will need to merely create an instance of this class, or optionally inherit from it and create an instance of the derived class.

A BasicConfigGroup object presents the interface of a Python sequence, containing a set of configlets in priority-sorted order. This sequence is created by the constructor using a directory path; this directory is search for subdirectories containing configlets, and each configlet found is instantiated and added to the sequence. The directory defaults to /usr/share/configlets, which means that the group will instantiate all configlets installed in the standard location by default.

In addition to the normal sequence interface, BasicConfigGroup provides other helper methods which act on all configlets within the group. These will be described below.

Starting Configlets

All configlets must be started using the start_configlet function. It takes one argument: the directory in which the configlet resides. It returns a fully instantiated configlet object.

Configlets are only meant to be instantiated once. If start_configlet is called more than once for the same directory, it will return a reference to the object it created previously, rather than creating a new one.

If you use BasicConfigGroup, it uses start_configlet to start all of the configlets it finds. Thus, you do not need to call start_configlet yourself. Be aware, however, that the configlets in BasicConfigGroup have been created this way, and in particular cannot be "recreated" outside the group.

Interacting with Debconf

In order for the configlets to be useful, they need to be fed data from the debconf database, and they need to be able to feed changes back to debconf. The front end is responsible for doing this, using two classes from the API: the privileged runner classes and the DebConf class.

The privileged runner classes are intended to provide an interface for front ends to perform tasks as root. They have a simple interface: a run function, that takes the command to run as the only argument. No return value is provided; run will throw a RuntimeError exception if there is any problem running the command. Because the interface is so simple, the privileged runner classes do not inherit; a class only needs to provide a run method in order to be a privileged runner.

The API provides two privileged runner classes: SimplePrivilegedRunner and GnomeSudoPrivilegedRunner. The default privileged runner, SimplePrivilegedRunner, simply assumes that the front end already has root privileges and runs commands accordingly. GnomeSudoPrivilegedRunner uses the gnome-sudo command to obtain root to run the command; this provides a nice graphical password dialog to the user when root privilege is needed and caches the successful authentication for subsequent commands. See the gnome-sudo documentation for setting this up properly.

The DebConf class does the actual work of getting and setting the debconf information. A proper DebConf object for the version of debconf can be obtained by calling get_debconf, passing in the list of packages that the configlets configure. (This information can be retrieved by calling get_packages on the configlet, and calling get_packages on a BasicConfigGroup will get all of the packages configured by all of the configlets in the group.) As the DebConf class uses a privileged runner class to do its work, you should set an appropriate privileged runner class for it by calling the set_privileged_runner method, passing in the privileged runner object as an argument.

Once the DebConf object is set up, it provides three methods: get, set, and commit. The get method returns an array of strings containing the debconf data in the format the configlets need. The set method takes a similarly formatted array of strings as debconf data and sets it in the database. The commit method takes it upon itself to cause the configured packages to reread their debconf data and reconfigure themselves accordingly.

Once debconf information is read, it must be passed to the configlets. This is done by taking the data returned from the get method and passing it to the configlet's load_debconf method. The configlet will search through the data, finding the appropriate values it needs and saving them internally in some way.

Saving debconf information involves several more steps. First, all configlets must be told to prepare for shutdown; this is done with the on_gnome_close method. Then, call report_debconf on each configlet, merge the results into a single array of strings, and pass that array to DebConf's set method. Finally, call DebConf.commit to cause the packages to reconfigure.

The BasicConfigGroup class does much of this work for you. By calling the load_all_debconf method, you can cause the group to load the debconf information and send it to all of the configlets in the group. Similarly, the save_and_commit_all_debconf method will read all of the debconf information from the configlets, save it to the debconf database, and commit it. The BasicConfigGroup takes care of creating a DebConf object and managing it for you; you should, however, create an appropriate privileged runner and set it for the group by calling the group's set_privileged_runner method on it. As an alternative, you can get access to the group's DebConf object with the get_debconf method; you can then iterate across all of the configlets in the group to load or save data, or use the group's load_debconf and report_debconf methods, which do the iteration for you.

Appendix A. API Reference

The API reference is here.