Plugins

Differences between revisions 8 and 9
Revision 8 as of 2009-07-20 13:05:41
Size: 8882
Editor: 65-78-0-53
Comment: add BEFORE/AFTER=None logic
Revision 9 as of 2009-07-20 14:23:08
Size: 9577
Editor: 65-78-0-53
Comment: Add progress info
Deletions are marked like this. Additions are marked like this.
Line 99: Line 99:
=== Open Questions ===
 * What about the apply() method and progress? Do we want to expose a string to show to user while Ubiquity is apply'ing the plugins? And allow progress reporting? Or do we just say 'plugins should apply quickly'?
=== Applying the Page ===

The page will provide an {{{apply()}}} method that will be called during install time. It will be passed a progress object to pass back progress information. Any strings will be provided via debconf templates.

==== Progress API ====

{{{
#!Python
def start(self, start=None, end=None):
    """If start or end is None, treat it as a pulsing progress.
       Can be called multiple times"""
    pass

def pulse(self, val=None):
    """Sets the progress to val, or just pulses"""
    pass
}}}

==== Apply Example ====

{{{
#!Python
  def apply(self, progress):
    progress.start('foo/applying', 0, 10)
    os.system('echo hi')
    progress.pulse(5)
    os.system('echo bye')
    progress.pulse(10)
    progress.start('foo/cleaning')
    os.system('sweeping')
    progress.pulse()
}}}
Line 106: Line 136:
#!Python
Line 141: Line 172:
    def apply(self):     def apply(self, progress):
        progress.start('foo/applying', 0, 10)
Line 147: Line 179:
        progress.set(10)

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

It can install the UI files (.glade, .ui) anywhere it wants. 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 the frontend-specific UI object (for GTK, a tuple of the top-level name and the glade xml object). The GTK frontend will load and connect the glade file itself.

How the plugin is translated

Can use its own debconf template. Ubiquity will do the translation for it, from its template.

How to find the plugin

Plugins can drop the filter in /usr/lib/ubiquity/plugins.d/

How to add/remove pages

There are four major options here:

  1. Require some third party to specify a hard list of pages. This could be done via preseeding the appropriate page list that Ubiquity uses (there's one for Ubiquity and one for OEM-config).
  2. Only allow one plugin (though it could still use multiple pages). This plugin would own the mechanism for specifying a page list (much like the above, but could be via a well-known file like /etc/ubiquity/pages).
  3. Have some syntax for saying a page should be before this page, but after this page. Sort of like how upstart lets your order jobs, but way simpler. Could reference either standard pages or other plugin pages. If Ubiquity can't figure out how to order the plugin, it just won't show it. Additionally would need to allow a plugin to list pages it wants to hide.
  4. Use filesystem ordering, like have plugins use plugins.d/10-mypage and 22-otherpage. The standard pages would need to have standard numbers. Which has all sorts of difficulties if we ever tried to add, remove, or reorder a standard page. All plugins would need to change. Could use a separate directory like plugins.hide.d/mypage to hide pages.

The first two options above are best suited for OEM installs, where one entity is crafting the experience. But the latter two options are slightly better for Ubuntu itself where it's easier for derivatives and packagers to plug into the existing system on a dynamic 'am-I-installed' basis. Seeds would do the rest of the job. Even here though, there is tight control over the experience. The first two options could still work.

I think option 3 above gives us the most flexibility for the least cost. The syntax can be dreadfully simple: only allow one 'after' or 'before' statement, and it would use a defined 'name' for the specified page (reducing complexity for things like the partitioner that have 2 UI files, but one module. So, for example:

AFTER='language'

Pages can be hidden from a HIDE list (a non-list would also work):

HIDE=['language', 'timezone']

Plugins would have a NAME field that specifies a name to use. The standard pages would have similar values. The recommended NAME for pages would be the module name that defines the page.

Ties (where two pages specify 'after language' for example) can be broken by file system ordering in plugins.d (ala option 4 above). Thus, Ubiquity just has to read those in in order, and everything just works. The number before the filename can be considered a priority then for ties. i.e. if both 10-mypage and 22-otherpage specify AFTER=language, 22 would be first, then 10.

  • If neither AFTER or BEFORE are specified, the page will not be used, but may still specify other plugin options, like HIDE.
  • If AFTER or BEFORE is a list, 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 HIDE 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.

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 tuple of (string, gtk.glade.XML), where the string specifies the name of the top-level page widget in the glade XML.

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 page widget.

Controller Design

The controller object passed to Page frontend classes is a simple mechanism for talking to the main frontend. Right now the only methods allowed are:

  • allow_go_forward(bool) (page starts with forward allowed)
  • allow_go_backward(bool) (page starts with backward allowed)

This may or may not be the main frontend object (which already has these functions), depending on how paranoid we are when implementing it.

Applying the Page

The page will provide an apply() method that will be called during install time. It will be passed a progress object to pass back progress information. Any strings will be provided via debconf templates.

Progress API

   1 def start(self, start=None, end=None):
   2     """If start or end is None, treat it as a pulsing progress.
   3        Can be called multiple times"""
   4     pass
   5 
   6 def pulse(self, val=None):
   7     """Sets the progress to val, or just pulses"""
   8     pass

Apply Example

   1   def apply(self, progress):
   2     progress.start('foo/applying', 0, 10)
   3     os.system('echo hi')
   4     progress.pulse(5)
   5     os.system('echo bye')
   6     progress.pulse(10)
   7     progress.start('foo/cleaning')
   8     os.system('sweeping')
   9     progress.pulse()

Example

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

   1 import gtk
   2 import os
   3 from ubiquity import Plugin
   4 
   5 # Plugin settings
   6 NAME='foo'
   7 BEFORE='usersetup'
   8 
   9 class PageGtk:
  10     def __init__(self, controller):
  11         self.controller = controller
  12         self.gladexml = gtk.glade.XML('/usr/share/ubiquity-foo/foo.glade', 'foo')
  13     def get_ui(self):
  14         return ('foo', self.gladexml)
  15 
  16     # Custom foo controls
  17     def get_bar(self):
  18         button = self.gladexml.get_widget('bar')
  19         return button.get_active()
  20     def set_bar(self, on):
  21         button = self.gladexml.get_widget('bar')
  22         button.set_active(on)
  23 
  24 class Page(Plugin):
  25     def prepare(self, unfiltered=False):
  26         # self.frontend is PageGtk above
  27         bar = self.db.get('foo/bar')
  28         self.frontend.set_bar(bar)
  29         return ([], ['foo/bar'])
  30 
  31     def ok_handler(self):
  32         bar = self.frontend.get_bar()
  33         self.preseed_bool('foo/bar', bar)
  34         Plugin.ok_handler(self)
  35 
  36     def apply(self, progress):
  37         progress.start('foo/applying', 0, 10)
  38         bar = self.db.get('foo/bar')
  39         if bar:
  40             os.system(['touch', '/etc/bar'])
  41         else:
  42             os.system(['rm', '/etc/bar'])
  43         progress.set(10)

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