Plugins
8619
Comment:
|
← Revision 29 as of 2013-01-16 10:57:22 ⇥
11920
|
Deletions are marked like this. | Additions are marked like this. |
Line 1: | Line 1: |
Here, I describe a possible system for enabling drop-in plugins for Ubiquity and OEM-config. | This is the method for enabling drop-in plugins for Ubiquity and OEM-config. |
Line 7: | Line 7: |
* The ability to stop going forward (say, a EULA page that requires an approval checkbox). Might as well through in stopping going back. | * The ability to prevent going forward (say, a EULA page that requires an approval checkbox). Might as well also allow preventing going back. |
Line 14: | Line 14: |
* Allow plugins to be pure Python (no required shell) | |
Line 32: | Line 33: |
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. | 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, certain member data and methods may be expected depending on which frontend is being used. Any such member will be prefixed with 'plugin_'. For example, with the GTK+ frontend, 'plugin_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. |
Line 36: | Line 41: |
Can use its own debconf template. Ubiquity will do the translation for it, from its template. | Plugins can use their own debconf template. Ubiquity will do the translation for it, from its template. |
Line 40: | Line 45: |
Plugins can drop the filter in /usr/lib/ubiquity/plugins.d/ === How to add/remove pages === There are four major options here: 0. 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). 0. 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). 0. 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. 0. 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. |
Plugins drop python page files in /usr/lib/ubiquity/plugins/ === 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. |
Line 69: | Line 65: |
* 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). * This allows |
* 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 the attribute WEIGHT in the module. If not defined, it is 0. i.e. if both mypage (WEIGHT=10) and otherpage (WEIGHT=22) specify {{{AFTER=language}}}, otherpage would be first in the resulting sequence, then mypage. Most Ubiquity built-in plugins are around WEIGHT=10. |
Line 74: | Line 75: |
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. |
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. Here's a list of supported member fields: * '''plugin_widgets''': A GTK+ widget or a list of widgets to show as pages * '''plugin_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 {{{plugin_get_current_page}}} as described below. * '''plugin_is_language''': Indicates that 'plugin_widgets' describes a page that will possibly change the language. This is used to cache all possible translations for the widgets on the page. * '''plugin_prefix''': Indicates a debconf template prefix to use for widget names when translating. Ubiquity will look up 'plugin_prefix/name' and use the string debconf gives as the UI translation. It may provide a method called {{{plugin_get_current_page(self)}}} that will return which of the widgets in '''plugin_widgets''' or '''plugin_optional_widgets''' should be shown now. If not defined, the first in the lists will be used. |
Line 82: | Line 90: |
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. |
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. Here's a list of supported member fields: * '''plugin_widgets''': A Qt widget or a list of widgets to show as pages * '''plugin_breadcrumb''': A debconf template key for the list of stages on the left of the UI. Setting 'plugin_breadcrumb' to None indicates that you don't want the breadcrumb list visible for your page. * '''plugin_is_language''': Indicates that 'plugin_widgets' describes a page that will possibly change the language. This is used to cache all possible translations for the widgets on the page. * '''plugin_prefix''': Indicates a debconf template prefix to use for widget names when translating. Ubiquity will look up 'plugin_prefix/name' and use the string debconf gives as the UI translation. It may provide a method called {{{plugin_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 90: | Line 105: |
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. === 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'? |
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: * add_builder(self, builder) (register a gtk.Builder instance created by the plugin; GTK frontend only) * 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) * toggle_top_level(self) (Hides or Shows the top level widget 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 and query debconf. Each plugin takes up 1% on the install progress bar. {{{ #!Python def info(self, title): '''Sets the progress to a debconf text description''' pass def get(self, question): '''Retrieves a debconf question's answer''' pass def substitute(self, template, substr, data): '''Substitutes substr with data in template''' pass }}} ==== Install Example ==== {{{ #!Python def install(self, target, progress, *args, **kwargs): progress.info('ubiquity/install/timezone') return InstallPlugin.install(self, target, progress, *args, **kwargs) }}} |
Line 102: | Line 162: |
In {{{/usr/lib/ubiquity/plugins.d/30-foo.py}}} {{{ import gtk |
In {{{/usr/lib/ubiquity/plugins/foo.py}}} {{{ #!Python |
Line 106: | Line 166: |
from ubiquity import Plugin | from ubiquity.plugin import * |
Line 110: | Line 170: |
BEFORE='usersetup' class PageGtk: def __init__(self, controller): |
AFTER='language' WEIGHT=30 class PageGtk(PluginUI): plugin_prefix = 'tcpd' plugin_title = 'ubiquity/text/ready_details_label' def __init__(self, controller, *args, **kwargs): import gtk |
Line 115: | Line 180: |
self.gladexml = gtk.glade.XML('/usr/share/ubiquity-foo/foo.glade', 'foo') def get_ui(self): return ('foo', self.gladexml) # Custom foo controls def get_bar(self): button = self.gladexml.get_widget('bar') return button.get_active() def set_bar(self, on): button = self.gladexml.get_widget('bar') button.set_active(on) |
self.page = gtk.CheckButton() self.page.set_name("paranoid-mode") self.plugin_widgets = self.page # Custom controls def get_mode(self): return self.page.get_active() def set_mode(self, on): self.page.set_active(on) class PageKde(PluginUI): # Just chosen because it's short and different. You can use # any template name here -- it's not affected by prefix. plugin_breadcrumb = 'ubiquity/text/ready_details_label' plugin_prefix = 'tcpd' def __init__(self, controller, *args, **kwargs): from PyQt4.QtGui import QCheckBox self.controller = controller self.page = QCheckBox() self.page.setObjectName("paranoid-mode") self.plugin_widgets = self.page # 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 129: | Line 213: |
# self.frontend is PageGtk above bar = self.db.get('foo/bar') self.frontend.set_bar(bar) return ([], ['foo/bar']) |
# self.ui is PageGtk above mode = self.db.get('tcpd/paranoid-mode') self.ui.set_mode(mode == 'true') return Plugin.prepare(self, unfiltered=unfiltered) |
Line 135: | Line 219: |
bar = self.frontend.get_bar() self.preseed_bool('foo/bar', bar) |
mode = self.ui.get_mode() self.preseed_bool('tcpd/paranoid-mode', mode) |
Line 139: | Line 223: |
def apply(self): bar = self.db.get('foo/bar') if bar: os.system(['touch', '/etc/bar']) else: os.system(['rm', '/etc/bar']) }}} |
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(self, target, progress, *args, **kwargs) }}} |
This is the method 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, certain member data and methods may be expected depending on which frontend is being used. Any such member will be prefixed with 'plugin_'. For example, with the GTK+ frontend, 'plugin_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/
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 the attribute WEIGHT in the module. If not defined, it is 0. i.e. if both mypage (WEIGHT=10) and otherpage (WEIGHT=22) specify AFTER=language, otherpage would be first in the resulting sequence, then mypage.
Most Ubiquity built-in plugins are around WEIGHT=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.
Here's a list of supported member fields:
plugin_widgets: A GTK+ widget or a list of widgets to show as pages
plugin_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 plugin_get_current_page as described below.
plugin_is_language: Indicates that 'plugin_widgets' describes a page that will possibly change the language. This is used to cache all possible translations for the widgets on the page.
plugin_prefix: Indicates a debconf template prefix to use for widget names when translating. Ubiquity will look up 'plugin_prefix/name' and use the string debconf gives as the UI translation.
It may provide a method called plugin_get_current_page(self) that will return which of the widgets in plugin_widgets or plugin_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.
Here's a list of supported member fields:
plugin_widgets: A Qt widget or a list of widgets to show as pages
plugin_breadcrumb: A debconf template key for the list of stages on the left of the UI. Setting 'plugin_breadcrumb' to None indicates that you don't want the breadcrumb list visible for your page.
plugin_is_language: Indicates that 'plugin_widgets' describes a page that will possibly change the language. This is used to cache all possible translations for the widgets on the page.
plugin_prefix: Indicates a debconf template prefix to use for widget names when translating. Ubiquity will look up 'plugin_prefix/name' and use the string debconf gives as the UI translation.
It may provide a method called plugin_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:
- add_builder(self, builder) (register a gtk.Builder instance created by the plugin; GTK frontend only)
- 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)
- toggle_top_level(self) (Hides or Shows the top level widget 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 and query debconf. Each plugin takes up 1% on the install progress bar.
Install Example
Example
In /usr/lib/ubiquity/plugins/foo.py
1 import os
2 from ubiquity.plugin import *
3
4 # Plugin settings
5 NAME='foo'
6 AFTER='language'
7 WEIGHT=30
8
9 class PageGtk(PluginUI):
10 plugin_prefix = 'tcpd'
11 plugin_title = 'ubiquity/text/ready_details_label'
12
13 def __init__(self, controller, *args, **kwargs):
14 import gtk
15 self.controller = controller
16 self.page = gtk.CheckButton()
17 self.page.set_name("paranoid-mode")
18 self.plugin_widgets = self.page
19
20 # Custom controls
21 def get_mode(self):
22 return self.page.get_active()
23 def set_mode(self, on):
24 self.page.set_active(on)
25
26 class PageKde(PluginUI):
27 # Just chosen because it's short and different. You can use
28 # any template name here -- it's not affected by prefix.
29 plugin_breadcrumb = 'ubiquity/text/ready_details_label'
30 plugin_prefix = 'tcpd'
31
32 def __init__(self, controller, *args, **kwargs):
33 from PyQt4.QtGui import QCheckBox
34 self.controller = controller
35 self.page = QCheckBox()
36 self.page.setObjectName("paranoid-mode")
37 self.plugin_widgets = self.page
38
39 # Custom controls
40 def get_mode(self):
41 from PyQt4.QtCore import Qt
42 return self.page.checkState() == Qt.Checked
43 def set_mode(self, on):
44 from PyQt4.QtCore import Qt
45 self.page.setCheckState(Qt.Checked if on else Qt.Unchecked)
46
47 class Page(Plugin):
48 def prepare(self, unfiltered=False):
49 # self.ui is PageGtk above
50 mode = self.db.get('tcpd/paranoid-mode')
51 self.ui.set_mode(mode == 'true')
52 return Plugin.prepare(self, unfiltered=unfiltered)
53
54 def ok_handler(self):
55 mode = self.ui.get_mode()
56 self.preseed_bool('tcpd/paranoid-mode', mode)
57 Plugin.ok_handler(self)
58
59 class Install(InstallPlugin):
60 def install(self, target, progress, *args, **kwargs):
61 progress.info('tcpd/paranoid-mode') # not really appropriate, but just an example
62 cmd = 'touch' if self.db.get('tcpd/paranoid-mode') else 'rm'
63 rv = os.system('%s %s' % (cmd, os.path.join(target, '/etc/bar')))
64 import time
65 time.sleep(45) # just to give you time to see this in the progress bar
66 if rv:
67 return rv
68 return InstallPlugin.install(self, target, progress, *args, **kwargs)
Ubiquity/Plugins (last edited 2013-01-16 10:57:22 by fourdollars)