Plugins

Differences between revisions 15 and 16
Revision 15 as of 2009-08-21 13:00:18
Size: 9260
Editor: 65-78-0-53
Comment:
Revision 16 as of 2009-08-21 19:54:35
Size: 11543
Editor: 65-78-0-53
Comment:
Deletions are marked like this. Additions are marked like this.
Line 33: Line 33:
The plugin will have UI classes like 'PageGtk' or 'PageKde' or 'PageNoninteractive' which will be instantiated by Ubiquity and passed to the filter class. If the right class for the current frontend isn't find, the plugin is ignored. For GUI frontends, a method {{{get_ui()}}} can be called to get frontend-specific objects and info. This will return a dictionary object with keys that the frontend knows. (For GTK, 'widgets' containing a list of pages and 'optional_widgets'). The plugin will have UI classes like 'PageGtk' or 'PageKde' or 'PageNoninteractive' which will be instantiated by Ubiquity and passed to the filter class. If the right class for the current frontend isn't find, the plugin is ignored. For GUI frontends, a method {{{get_ui()}}} can be called to get frontend-specific objects and info. This will return a dictionary object with keys that the frontend knows. For GTK+, 'widgets' containing a list of pages and other information.  See below for more details.
Line 75: Line 75:
It must provide a method called {{{get_ui(self)}}} that returns a dictionary with 'widgets' and optionally, 'optional_widgets' fields. 'widgets' is a list or singleton of GTK pages (usually just a single page). It must provide a method called {{{get_ui(self)}}} that returns a dictionary. Here's a list of the supported keys:

 * '''widgets''': A GTK+ widget or a list of widgets to show as pages
 * '''optional_widgets''': A GTK+ widget or a list of widgets that aren't likely to be shown. These widgets don't add to the step count in the bottom left of the UI (step X of Y), but can be shown via {{{get_current_page}}} as described below.
 * '''is_language_page''': Indicates that 'widgets' describes a page that will possibly change the language. This is used to cache all possible translations for the widgets on the page.
 * '''prefix''': Indicates a debconf template prefix to use for widget names when translating. Ubiquity will look up 'prefix/name' and use the string debconf gives as the UI translation.

It may provide a method called {{{get_current_page(self)}}} that will return which of the widgets in '''widgets''' or '''optional_widgets''' should be shown now. If not defined, the first in the lists will be used.
Line 83: Line 90:
It must provide a method called {{{get_ui(self)}}} that returns a dictionary with 'widgets' and 'breadcrumb' fields. 'widgets' is a list or singleton of Qt pages (usually just a single page). 'breadcrumb' is a debconf template key for the list of stages. Setting 'breadcrumb' to None indicates that you don't want the breadcrumb list visible for your page. It must provide a method called {{{get_ui(self)}}} that returns a dictionary. Here's a list of the supported keys:

 * '''widgets''': A Qt widget or a list of widgets to show as pages
 * '''breadcrumb''': A debconf template key for the list of stages on the left of the UI. Setting 'breadcrumb' to None indicates that you don't want the breadcrumb list visible for your page.
 * '''is_language_page''': Indicates that 'widgets' describes a page that will possibly change the language. This is used to cache all possible translations for the widgets on the page.
 * '''prefix''': Indicates a debconf template prefix to use for widget names when translating. Ubiquity will look up 'prefix/name' and use the string debconf gives as the UI translation.

It may provide a method called {{{get_current_page(self)}}} that will return which of the widgets in '''widgets''' should be shown now. If not defined, the first in the lists will be used.
Line 87: Line 101:
The controller object passed to Page UI classes is a simple mechanism for talking to the main frontend.  Right now the only methods allowed are:
The controller object passed to Page UI classes is a simple mechanism for talking to the main frontend.

Here
are the currently defined members:
Line 90: Line 105:
 * oem_config, oem_user_config (flags for which mode we're in)
 * is_language_page (set this to True if you intend to change languages)
 * oem_config
 *
oem_user_config (flags for which mode we're in)

A
nd the methods:
Line 132: Line 149:
#!Python
import os
#!Pythonimport os
Line 138: Line 154:
BEFORE='usersetup' AFTER='language'
Line 144: Line 160:
        self.builder = gtk.Builder()
        self.builder.add_from_file('/usr/share/ubiquity-foo/foo.ui')
        self.builder.connect_signals(self)
        self.page = gtk.CheckButton()
        self.page.set_name("paranoid-mode")
Line 148: Line 164:
        return self.builder.get_object('foo')

    # Custom foo controls
    def get_bar(self):
        button = self.builder.get_object('bar')
        return button.get_active()
    def set_bar(self, on):
        button = self.builder.get_object('bar')
        button.set_active(on)
        return {'widgets': self.page, 'prefix': 'tcpd'}

    # Custom controls
    def get_mode(self):
        return self.page.get_active()
    def set_mode(self, on):
        self.page.set_active(on)

class PageKde:
    def __init__(self, controller):
        from PyQt4.QtGui import QCheckBox
        self.controller = controller
        self.page = QCheckBox()
        self.page.setObjectName("paranoid-mode")

    def get_ui(self):
        return {'widgets': self.page,
                # Just chosen because it's short and different. You can use
                # any template name here -- it's not affected by prefix.
                'breadcrumb': 'ubiquity/text/ready_details_label',
                'prefix': 'tcpd'}

    # Custom controls
    def get_mode(self):
        from PyQt4.QtCore import Qt
        return self.page.checkState() == Qt.Checked
    def set_mode(self, on):
        from PyQt4.QtCore import Qt
        self.page.setCheckState(Qt.Checked if on else Qt.Unchecked)
Line 161: Line 197:
        bar = self.db.get('foo/bar')
        self.ui.set_bar(bar)
        return ([], ['foo/bar'])
        mode = self.db.get('tcpd/paranoid-mode')
        self.ui.set_mode(mode == 'true')
        return Plugin.prepare(self, unfiltered=unfiltered)
Line 166: Line 202:
        bar = self.ui.get_bar()
        self.preseed_bool('foo/bar', bar)
        mode = self.ui.get_mode()
        self.preseed_bool('tcpd/paranoid-mode', mode)
Line 172: Line 208:
        progress.info('foo/applying')
        cmd = 'touch' if self.db.get('foo/bar') else 'rm'
        rv = os.system([cmd, os.path.join(target, '/etc/bar')])
        progress.info('tcpd/paranoid-mode') # not really appropriate, but just an example
        cmd = 'touch' if self.db.get('tcpd/paranoid-mode') else 'rm'
        rv = os.system('%s %s' % (cmd, os.path.join(target, '/etc/bar')))
        import time
        time.sleep(45) # just to give you time to see this in the progress bar

Here, I describe a possible system for enabling drop-in plugins for Ubiquity and OEM-config.

Requirements

  • Insert a page: The ability to add a page anywhere in the page sequence
  • Hide a page: The ability to stop a standard page from showing up (the combination of inserting/hiding lets you replace a page)
  • The ability to prevent going forward (say, a EULA page that requires an approval checkbox). Might as well also allow preventing going back.
  • Let plugins be translatable
  • Let plugins execute code after user is done entering any info, to actually do what user asks
  • Let plugins offer gtk or kde UI (or debconf or noninteractive, etc if desired)

Wants

  • Reduce plugin's exposure to Ubiquity internals, to make them more change-proof.
  • Allow plugins to be pure Python (no required shell)

Design

What a plugin looks like

Bits of a standard gtk page:

  • A glade file
  • A debconf filter
  • Ask and apply scripts

With the above, by providing the above files, we can do everything a normal page can in a plugin. Assuming Ubiquity knows how to find the files.

The asking can be provided by Ubiquity, as long as the plugin gives us a list of debconf questions to ask. And the apply script could just be done by the filter by a special 'apply' method. Then all a plugin needs is a filter and a UI file.

We could additionally dumb down the python a plugin needs to provide by having them be 'filter lites' custom classes that didn't do everything a filter does. But the filter parent classes already provide good default methods. We should probably have a plugin parent class that sits between the debconf filter and the plugin, just to provide further default methods and any special configuration we need.

How the plugin will talk to its own UI

The plugin will have UI classes like 'PageGtk' or 'PageKde' or 'PageNoninteractive' which will be instantiated by Ubiquity and passed to the filter class. If the right class for the current frontend isn't find, the plugin is ignored. For GUI frontends, a method get_ui() can be called to get frontend-specific objects and info. This will return a dictionary object with keys that the frontend knows. For GTK+, 'widgets' containing a list of pages and other information. See below for more details.

The plugin can install the UI files (.glade, .ui) anywhere it wants, because it is responsible for loading it. The GTK and KDE frontends only want to see actual widgets.

How the plugin is translated

Plugins can use their own debconf template. Ubiquity will do the translation for it, from its template.

How to find the plugin

Plugins drop python page files in /usr/lib/ubiquity/plugins.d/

How to order/add/remove pages

Plugins has a module-wide NAME field that specifies a name to use. The recommended NAME for pages would be similar to the module name that defines the page. For third-party plugins, using a namespace (e.g. 'foo-page' and 'foo-page2') is recommended.

To insert your page into the sequence of pages, use AFTER or BEFORE fields.

AFTER='goober-language'
BEFORE=['language', None]

Pages can be hidden from a HIDDEN field (a list would also work):

HIDDEN='language'
  • If neither AFTER or BEFORE are specified, the page will not be used, but may still specify other plugin options, like HIDDEN. (i.e. a page that only hides, and does not insert itself)
  • If AFTER or BEFORE is a list of names, the first component that Ubiquity recognizes is used.
  • If both AFTER and BEFORE are specified, Ubiquity will use AFTER if it recognizes it, else use BEFORE.
  • If Ubiquity still can't find a value that it recognizes, the page is ignored (and other plugin options like HIDDEN are not used -- it's likely this page is optional, pending installation of some package we don't have or was written for an older Ubiquity).

  • If AFTER's value is None or otherwise 'Python-false', it is inserted at the beginning of the page sequence.
  • If BEFORE's value is None or otherwise 'Python-false', it is inserted at the end of the page sequence.

Ties (where two pages specify 'after language' for example) are broken by file system ordering in plugins.d. Ubiquity reads the plugins.d files in order, and resolves names as they come in. The number before the filename can thus be considered a priority then for ties. i.e. if both 10-mypage and 22-otherpage specify AFTER=language, 22 would be first in the resulting sequence, then 10.

PageGtk Design

The PageGtk class must provide an init that takes at least one argument. Additional keyword arguments are allowed, but not currently used.

This first argument is a 'controller' class, specified below. This is how PageGtk can provide feedback to the 'real' frontend.

It must provide a method called get_ui(self) that returns a dictionary. Here's a list of the supported keys:

  • widgets: A GTK+ widget or a list of widgets to show as pages

  • optional_widgets: A GTK+ widget or a list of widgets that aren't likely to be shown. These widgets don't add to the step count in the bottom left of the UI (step X of Y), but can be shown via get_current_page as described below.

  • is_language_page: Indicates that 'widgets' describes a page that will possibly change the language. This is used to cache all possible translations for the widgets on the page.

  • prefix: Indicates a debconf template prefix to use for widget names when translating. Ubiquity will look up 'prefix/name' and use the string debconf gives as the UI translation.

It may provide a method called get_current_page(self) that will return which of the widgets in widgets or optional_widgets should be shown now. If not defined, the first in the lists will be used.

PageKde Design

The PageKde class must provide an init that takes at least one argument. Additional keyword arguments are allowed, but not currently used.

This first argument is a 'controller' class, specified below. This is how PageKde can provide feedback to the 'real' frontend.

It must provide a method called get_ui(self) that returns a dictionary. Here's a list of the supported keys:

  • widgets: A Qt widget or a list of widgets to show as pages

  • breadcrumb: A debconf template key for the list of stages on the left of the UI. Setting 'breadcrumb' to None indicates that you don't want the breadcrumb list visible for your page.

  • is_language_page: Indicates that 'widgets' describes a page that will possibly change the language. This is used to cache all possible translations for the widgets on the page.

  • prefix: Indicates a debconf template prefix to use for widget names when translating. Ubiquity will look up 'prefix/name' and use the string debconf gives as the UI translation.

It may provide a method called get_current_page(self) that will return which of the widgets in widgets should be shown now. If not defined, the first in the lists will be used.

Controller Design

The controller object passed to Page UI classes is a simple mechanism for talking to the main frontend.

Here are the currently defined members:

  • dbfilter (lets you have access to your Page class)
  • oem_config
  • oem_user_config (flags for which mode we're in)

And the methods:

  • translate(self, lang=None, just_me=True, reget=False) (retranslate the UI)
  • allow_go_forward(self, bool) (page starts with forward allowed)
  • allow_go_backward(self, bool) (page starts with backward allowed)
  • go_forward(self) (clicks forward)
  • go_backward(self) (clicks back)
  • go_to_page(self, page) (jumps to the given page)

Applying the Page

If your plugin wants to perform install-time work, it can define a Install class that derives from the InstallPlugin class.

InstallPlugin is a Plugin and follows the same conventions -- it is a debconf filter that has prepare and cleanup methods. But additionally and more relevantly for us, it adds an install method.

install is passed a target directory (usually '/target' or '/'), a progress class (more on that below), and possibly some future arguments. It's recommended that plugins use *args and **kwargs to gracefully handle future extensions.

If install returns a Python-true value, it is considered an error (ala command line programs). The error value (a string or a number) will be printed to syslog.

Progress API

install is passed a progress class that can provide updates on what your plugin is doing. Currently, it only supports passing a text description. Each plugin takes up 1% on the install progress bar.

   1 def info(self, title):
   2     pass

Install Example

   1 def install(self, target, progress, *args, **kwargs):
   2     progress.info('ubiquity/install/timezone')
   3     return InstallPlugin.install(target, progress, *args, **kwargs)

Example

In /usr/lib/ubiquity/plugins.d/30-foo.py

from ubiquity.plugin import *

# Plugin settings
NAME='foo'
AFTER='language'

class PageGtk:
    def __init__(self, controller):
        import gtk
        self.controller = controller
        self.page = gtk.CheckButton()
        self.page.set_name("paranoid-mode")

    def get_ui(self):
        return {'widgets': self.page, 'prefix': 'tcpd'}

    # Custom controls
    def get_mode(self):
        return self.page.get_active()
    def set_mode(self, on):
        self.page.set_active(on)

class PageKde:
    def __init__(self, controller):
        from PyQt4.QtGui import QCheckBox
        self.controller = controller
        self.page = QCheckBox()
        self.page.setObjectName("paranoid-mode")

    def get_ui(self):
        return {'widgets': self.page,
                # Just chosen because it's short and different.  You can use
                # any template name here -- it's not affected by prefix.
                'breadcrumb': 'ubiquity/text/ready_details_label',
                'prefix': 'tcpd'}

    # Custom controls
    def get_mode(self):
        from PyQt4.QtCore import Qt
        return self.page.checkState() == Qt.Checked
    def set_mode(self, on):
        from PyQt4.QtCore import Qt
        self.page.setCheckState(Qt.Checked if on else Qt.Unchecked)

class Page(Plugin):
    def prepare(self, unfiltered=False):
        # self.frontend is PageGtk above
        mode = self.db.get('tcpd/paranoid-mode')
        self.ui.set_mode(mode == 'true')
        return Plugin.prepare(self, unfiltered=unfiltered)

    def ok_handler(self):
        mode = self.ui.get_mode()
        self.preseed_bool('tcpd/paranoid-mode', mode)
        Plugin.ok_handler(self)

class Install(InstallPlugin):
    def install(self, target, progress, *args, **kwargs):
        progress.info('tcpd/paranoid-mode') # not really appropriate, but just an example
        cmd = 'touch' if self.db.get('tcpd/paranoid-mode') else 'rm'
        rv = os.system('%s %s' % (cmd, os.path.join(target, '/etc/bar')))
        import time
        time.sleep(45) # just to give you time to see this in the progress bar
        if rv:
            return rv
        return InstallPlugin.install(target, progress, *args, **kwargs)

Ubiquity/Plugins (last edited 2013-01-16 10:57:22 by fourdollars)