5. Developing Installer Add-ons
5.1. Introduction to Anaconda and Add-ons
5.1.1. Introduction to Anaconda
Gtk
widgets (written in C), systemd
units, and dracut
libraries. Together, they form a tool that allows users to set parameters of the resulting (target) system and then set such a system up on a machine. The installation process has four major steps:
- installation destination preparation (usually disk partitioning)
- package and data installation
- boot loader installation and configuration
- configuration of the newly installed system
VNC
, which allows you to use the GUI even on systems with no graphics cards or even attached monitor. However, there are still cases where this is not desired, but at the same time, you may want to perform an interactive installation. For these cases, a text mode (TUI) is available. The TUI works in a way similar to a monochrome line printer, which allows it to work even on serial consoles which do not support cursor movement, colors and other advanced features. The text mode is limited in that it only allows you to customize most common options, such as network settings, language options or installation (package) source; advanced features such as manual partitioning are not available in this interface.
5.1.2. Firstboot and Initial Setup
Gtk2
and the pygtk2
module. [1] For this reason, a new tool called Initial Setup was developed, which reuses code from Anaconda. This allows add-ons developed for Anaconda to be easily reused in Initial Setup. This topic is further discussed in Section 5.6, “Writing an Anaconda add-on”.
5.1.3. Anaconda and Initial Setup Add-ons
%addon com_redhat_kdump
command and its options) and is fully integrated as an additional screen in the text-based and graphical interfaces. You can develop other add-ons in the same way and add them to the default installer using procedures described further in this guide.
5.1.4. Additional Information
- The Anaconda page on Fedora Project Wiki contains provides more information about the installer.
- Information about development of Anaconda into its current version is available at the Anaconda/NewInstaller Wiki page.
- The Kickstart Installations chapter of the Red Hat Enterprise Linux 7 Installation Guide provides full documentation of Kickstart, including a list of all supported commands and options.
- The Installing Using Anaconda chapter of the Red Hat Enterprise Linux 7 Installation Guide describes the installation process in the graphical and text user interfaces.
- For information about tools used for after-installation configuration, see Initial Setup and Firstboot.
5.2. Architecture of Anaconda
pykickstart
- used to parse and validate Kickstart files and also to provide a data structure which stores values which drive the installationyum
- the package manager which handles installation of packages and resolving dependenciesblivet
- originally split from the anaconda package as pyanaconda.storage; used to handle all activities related to storage managementpyanaconda
- package containing the core of the user interface and modules for functionality unique to Anaconda, such as keyboard and timezone selection, network configuration, and user creation, as well as a number of utilities and system-oriented functionspython-meh
- contains an exception handler which gathers and stores additional system information in case of a crash and passes this information to thelibreport
library, which itself is a part of the ABRT Project.
pykickstart
module and imported into memory as a tree-like structure. If no Kickstart file is provided, an empty tree-like structure is created instead. If the installation is interactive (not all required Kickstart commands have been used), the structure is then updated with choices made by the user in the interactive interface.
/root/
directory on the installed system; therefore the installation can be replicated automatically by reusing this automatically generated Kickstart file.
pyanaconda.kickstart
module. An important rule which governs this behavior is that there is no place to store configuration data, and the installation process is data-driven and relies on transactions as much as possible. This enforces the following features:
- every feature of the installer must be supported in Kickstart
- there is a single, obvious point in the installation process where changes are written to the target system; before this point, no lasting changes (e.g. formatting storage) are made
- every change made manually in the user interface is reflected in the resulting Kickstart file and can be replicated
setup
method) to modify the runtime environment of the installation if necessary, and then executed (the execute
method) to perform the changes on the target system. These methods are further described in Section 5.6, “Writing an Anaconda add-on”.
5.3. The Hub & Spoke model
- users are not forced to go through the screens in some strictly defined order
- users are not forced to visit every screen no matter if they understand what the options configured in it mean or not
- it is good for the transactional mode where all desired values can be set while nothing is actually happening to the underlying machine until a special button is clicked
- it provides way to show an overview of the configured values
- it has a great support for extensibility, because additional spokes can be put on hubs without need to reorder anything and resolve some complex ordering dependencies
- it can be used for both graphical and text mode of the installer
Figure 2. Diagram of the hub and spoke model
Note
- The Installation Summary hub which shows a summary of configured options before the installation begins
- The Configuration and Progress hub which appears after you click Begin Installation in Installation Summary, and which displays the progress of the installation process and allows you to configure additional options (set the root password and create a user account).
- ready - states whether the spoke can be visited or not; for example, when the installer is configuring a package source, that spoke is not ready, is colored gray, and cannot be accessed until configuration is complete
- completed - marks the spoke as completed (all required values are set) or not
- mandatory - determines whether the spoke must be visited and confirmed by the user before continuing the installation; for example, the Installation Destination spoke must always be visited, even if you want to use automatic disk partitioning
- status - provides a short summary of values configured within the spoke (displayed under the spoke name in the hub)
5.4. Threads and Communication
GLib.idle_add
, which is not always easy or desired. To alleviate this problem, several helper functions and decorators are defined in the pyanaconda.ui.gui.utils module.
@gtk_action_wait
and @gtk_action_nowait
decorators. They change the decorated function or method in such a way that when this function or method is called, it is automatically queued into Gtk's main loop, run in the main thread, and the return value is either returned to the caller or dropped, respectively.
hubQ
, which is being periodically checked in the main event loop. When a spoke becomes accessible, it sends a message to this queue announcing this change and that it should no longer be blocked.
progressQ
which serves as a medium to transfer installation progress updates.
5.5. Anaconda Add-on Structure
__init__.py
and other source directories (subpackages) inside. Because Python allows importing each package name only once, the package top-level directory name must be unique. At the same time, the name can be arbitrary, because add-ons are loaded regardless of their name - the only requirement is that they must be placed in a specific directory.
_
) instead of dots so that the directory name is a valid identifier for a Python package. An example add-on name following these suggestions would therefore be e.g. com_example_hello_world
. This convention follows the recommended naming scheme for Python package and module names.
Important
__init__.py
file in each directory. Directories missing this file are not considered valid Python packages.
ks
for Kickstart, gui
for the graphical interface and tui
for the text-based interface. The gui
and tui
packages must also contain a spokes
subpackage. [3]
ks/
, gui/
and tui/
directories can contain Python modules with any name.
Example 2. Sample Add-on Structure
com_example_hello_world ├─ ks │ └─ __init__.py ├─ gui │ ├─ __init__.py │ └─ spokes │ └─ __init__.py └─ tui ├─ __init__.py └─ spokes └─ __init__.py
5.6. Writing an Anaconda add-on
5.6.1. Kickstart Support
com_example_hello_world/ks/
directory you have created previously, make sure it contains an __init__.py
file, and add another Python script named hello_world.py
.
%addon
statement and is closed by %end
. The %addon
line also contains the name of the add-on (such as %addon com_example_hello_world
) and optionally a list of arguments, if the add-on supports them.
Example 3. Using an Add-on in a Kickstart File
%addon ADDON_NAME [arguments] first line second line ... %end
AddonData
. This class is defined in pyanaconda.addons and represents an object for parsing and storing data from a Kickstart file.
AddonData
class. Anything between the first and last line is passed to the add-on's class one line at a time. To keep the example Hello World add-on simple, it will merge all lines in this block into a single line and separate the original lines with a space.
AddonData
with a method for handling the list of arguments from the %addon
line, and a method for handling lines inside the section. The pyanaconda/addons.py
module contains two methods which can be used for this:
handle_header
- takes a list of arguments from the%addon
line (and line numbers for error reporting)handle_line
- takes a single line of content from between the%addon
and%end
statements
Example 4. Using handle_header and handle_line
from pyanaconda.addons import AddonData from pykickstart.options import KSOptionParser # export HelloWorldData class to prevent Anaconda's collect method from taking # AddonData class instead of the HelloWorldData class # :see: pyanaconda.kickstart.AnacondaKSHandler.__init__ __all__ = ["HelloWorldData"] HELLO_FILE_PATH = "/root/hello_world_addon_output.txt" class HelloWorldData(AddonData): """ Class parsing and storing data for the Hello world addon. :see: pyanaconda.addons.AddonData """ def __init__(self, name): """ :param name: name of the addon :type name: str """ AddonData.__init__(self, name) self.text = "" self.reverse = False def handle_header(self, lineno, args): """ The handle_header method is called to parse additional arguments in the %addon section line. :param lineno: the current linenumber in the kickstart file :type lineno: int :param args: any additional arguments after %addon <name> :type args: list """ op = KSOptionParser() op.add_option("--reverse", action="store_true", default=False, dest="reverse", help="Reverse the display of the addon text") (opts, extra) = op.parse_args(args=args, lineno=lineno) # Reject any additoinal arguments. Since AddonData.handle_header # rejects any arguments, we can use it to create an error message # and raise an exception. if extra: AddonData.handle_header(self, lineno, extra) # Store the result of the option parsing self.reverse = opts.reverse def handle_line(self, line): """ The handle_line method that is called with every line from this addon's %addon section of the kickstart file. :param line: a single line from the %addon section :type line: str """ # simple example, we just append lines to the text attribute if self.text is "": self.text = line.strip() else: self.text += " " + line.strip()
__all__
variable which is necessary to prevent Anaconda's collect method from taking the AddonData
class instead of add-on specific HelloWorldData
.
HelloWorldData
class inherited from AddonData
with its __init__
method calling the parent's __init__
and initializing the attributes self.text
and self.reverse
to False
.
self.reverse
attribute is populated in the handle_header
method, and the self.text
is populated in handle_line
. The handle_header
method uses an instance of the KSOptionParser
provided by pykickstart
to parse additional options used on the %addon
line, and handle_line
strips the content lines of white space at the beginning and end of each line, and appends them to self.text
.
setup
- called before the installation transaction starts and used to make changes to the installation runtime environmentexecute
- called at the end of the transaction and used to make changes to the target system
Example 5. Importing the setup and execute Methods
import os.path from pyanaconda.addons import AddonData from pyanaconda.constants import ROOT_PATH HELLO_FILE_PATH = "/root/hello_world_addon_output.txt"
setup
and execute
methods included is below:
Example 6. Using the setup and execute Methods
def setup(self, storage, ksdata, instclass, payload): """ The setup method that should make changes to the runtime environment according to the data stored in this object. :param storage: object storing storage-related information (disks, partitioning, bootloader, etc.) :type storage: blivet.Blivet instance :param ksdata: data parsed from the kickstart file and set in the installation process :type ksdata: pykickstart.base.BaseHandler instance :param instclass: distribution-specific information :type instclass: pyanaconda.installclass.BaseInstallClass :param payload: object managing packages and environment groups for the installation :type payload: any class inherited from the pyanaconda.packaging.Payload class """ # no actions needed in this addon pass def execute(self, storage, ksdata, instclass, users, payload): """ The execute method that should make changes to the installed system. It is called only once in the post-install setup phase. :see: setup :param users: information about created users :type users: pyanaconda.users.Users instance """ hello_file_path = os.path.normpath(ROOT_PATH + HELLO_FILE_PATH) with open(hello_file_path, "w") as fobj: fobj.write("%s\n" % self.text)
setup
method does nothing; the Hello World add-on does not make any changes to the installation runtime environment. The execute
method writes stored text into a file created in the target system's root (/
) directory.
__str__
method recursively on the tree-like structure storing installation data, which means that the class inherited from AddonData
must define its own __str__
method which returns its stored data in valid Kickstart syntax. This returned data must be possible to parse again using pykickstart
.
__str__
method will be similar to the following example:
Example 7. Defining a __str__ Method
def __str__(self): """ What should end up in the resulting kickstart file, i.e. the %addon section containing string representation of the stored data. """ addon_str = "%%addon %s" % self.name if self.reverse: addon_str += "--reverse" addon_str += "\n%s\n%%end" % self.text return addon_str
handle_header
, handle_line
, setup
, execute
and __str__
), it becomes a valid Anaconda add-on. You can continue with the following sections to add support for the graphical and text-based user interfaces, or you can continue with Section 5.7, “Deploying and testing an Anaconda add-on” and test the add-on.
5.6.2. Graphical user interface
Note
SpokeWindow
.
5.6.2.1. Basic features
NormalSpoke
, which is defined in pyanaconda.ui.gui.spokes
. As the class name suggests, it is a class for the normal spoke type of screen as described in Section 5.3, “The Hub & Spoke model”.
NormalSpoke
, you must define the following class attributes which are required by the API:
builderObjects
- lists all top-level objects from the spoke's.glade
file that should be, with their children objects (recursively), exposed to the spoke - or should be an empty list if everything should be exposed to the spoke (not recommended)uiFile
- contains the name of the.glade
filecategory
- contains the class of the category the spoke belongs toicon
- contains the identifier of the icon that will be used for the spoke on the hubtitle
defines the title that will be used for the spoke on the hub
Example 8. Defining Attributes Required for the Normalspoke Class
# will never be translated _ = lambda x: x N_ = lambda x: x # the path to addons is in sys.path so we can import things from org_fedora_hello_world from org_fedora_hello_world.gui.categories.hello_world import HelloWorldCategory from pyanaconda.ui.gui.spokes import NormalSpoke # export only the spoke, no helper functions, classes or constants __all__ = ["HelloWorldSpoke"] class HelloWorldSpoke(NormalSpoke): """ Class for the Hello world spoke. This spoke will be in the Hello world category and thus on the Summary hub. It is a very simple example of a unit for the Anaconda's graphical user interface. :see: pyanaconda.ui.common.UIObject :see: pyanaconda.ui.common.Spoke :see: pyanaconda.ui.gui.GUIObject """ ### class attributes defined by API ### # list all top-level objects from the .glade file that should be exposed # to the spoke or leave empty to extract everything builderObjects = ["helloWorldSpokeWindow", "buttonImage"] # the name of the main window widget mainWidgetName = "helloWorldSpokeWindow" # name of the .glade file in the same directory as this source uiFile = "hello_world.glade" # category this spoke belongs to category = HelloWorldCategory # spoke icon (will be displayed on the hub) # preferred are the -symbolic icons as these are used in Anaconda's spokes icon = "face-cool-symbolic" # title of the spoke (will be displayed on the hub) title = N_("_HELLO WORLD")
__all__
attribute is used to export the spoke class, followed by the first lines of its definition including definitions of attributes mentioned above. The values of these attributes are referencing widgets defined in com_example_hello_world/gui/spokes/hello.glade
file.
category
, which has its value imported from the HelloWorldCategory
class from the com_example_hello_world.gui.categories
module. The HelloWorldCategory
class will be discussed later, but for now, note that the path to add-ons is in sys.path so that things can be imported from the com_example_hello_world package.
title
, which contains two underscores in its definition. The first one is part of the N_
function name which marks the string for translation, but returns the non-translated version of the string (translation is done later). The second underscore marks the beginning of the title itself and makes the spoke reachable using the Alt+H keyboard shortcut.
__init__
method and the initialize
method.
__init__
method should only call the parent's __init__
method and (for example) initialize non-GUI attributes. On the other hand, the initialize
method that is called when the installer's graphical user interface initializes should finish the full initialization of the spoke.
__init__
method):
Example 9. Defining the __init__ and initialize Methods
def __init__(self, data, storage, payload, instclass): """ :see: pyanaconda.ui.common.Spoke.__init__ :param data: data object passed to every spoke to load/store data from/to it :type data: pykickstart.base.BaseHandler :param storage: object storing storage-related information (disks, partitioning, bootloader, etc.) :type storage: blivet.Blivet :param payload: object storing packaging-related information :type payload: pyanaconda.packaging.Payload :param instclass: distribution-specific information :type instclass: pyanaconda.installclass.BaseInstallClass """ NormalSpoke.__init__(self, data, storage, payload, instclass) def initialize(self): """ The initialize method that is called after the instance is created. The difference between __init__ and this method is that this may take a long time and thus could be called in a separated thread. :see: pyanaconda.ui.common.UIObject.initialize """ NormalSpoke.initialize(self) self._entry = self.builder.get_object("textEntry")
data
parameter passed to the __init__
method. This is the in-memory tree-like representation of the Kickstart file where all data is stored. In one of the ancestors' __init__
methods it is stored in the self.data
attribute, which allows all other methods in the class to read and modify the structure.
HelloWorldData
class has already been defined in Section 5.6.1, “Kickstart Support”, there already is a subtree in self.data
for this add-on, and its root (an instance of the class) is available as self.data.addons.com_example_hello_world
.
__init__
does is initializing an instance of the GtkBuilder
with the spoke's .glade
file and storing it as self.builder
. This is used in the initialize
method to get the GtkTextEntry
used to show and modify the text from the kickstart file's %addon section.
__init__
and initialize
methods are both important when the spoke is created. However, the main role of the spoke is to be visited by an user who wants to change or review the values this spoke shows and sets. To enable this, three other methods are available:
refresh
- called when the spoke is about to be visited; This method refreshes the state of the spoke (mainly its UI elements) to make sure that current values stored in theself.data
structure are displayedapply
- called when the spoke is left and used to store values from UI elements back into theself.data
structureexecute
- called when the spoke is left and used to perform any runtime changes based on the new state of the spoke
Example 10. Defining the refresh, apply and execute Methods
def refresh(self): """ The refresh method that is called every time the spoke is displayed. It should update the UI elements according to the contents of self.data. :see: pyanaconda.ui.common.UIObject.refresh """ self._entry.set_text(self.data.addons.org_fedora_hello_world.text) def apply(self): """ The apply method that is called when the spoke is left. It should update the contents of self.data with values set in the GUI elements. """ self.data.addons.org_fedora_hello_world.text = self._entry.get_text() def execute(self): """ The excecute method that is called when the spoke is left. It is supposed to do all changes to the runtime environment according to the values set in the GUI elements. """ # nothing to do here pass
ready
- determines whether the spoke is ready to be visited; if the value is false, the spoke is not accessible (e.g. the Package Selection spoke before a package source is configured)completed
- determines if the spoke has been completedmandatory
- determines if the spoke is mandatory or not (e.g. the Installation Destination spoke, which must be always visited, even if you want to use automatic partitioning)
text
attribute of the HelloWorldData
class:
Example 11. Defining the ready, completed and mandatory Methods
@property def ready(self): """ The ready property that tells whether the spoke is ready (can be visited) or not. The spoke is made (in)sensitive based on the returned value. :rtype: bool """ # this spoke is always ready return True @property def completed(self): """ The completed property that tells whether all mandatory items on the spoke are set, or not. The spoke will be marked on the hub as completed or uncompleted acording to the returned value. :rtype: bool """ return bool(self.data.addons.org_fedora_hello_world.text) @property def mandatory(self): """ The mandatory property that tells whether the spoke is mandatory to be completed to continue in the installation process. :rtype: bool """ # this is an optional spoke that is not mandatory to be completed return False
status
exists; this property contains a single line of text with a short summary of configured values, which can then be displayed in the hub under the spoke title.
status
property is defined in the Hello World example add-on as follows:
Example 12. Defining the status Property
@property def status(self): """ The status property that is a brief string describing the state of the spoke. It should describe whether all values are set and if possible also the values themselves. The returned value will appear on the hub below the spoke's title. :rtype: str """ text = self.data.addons.org_fedora_hello_world.text # If --reverse was specified in the kickstart, reverse the text if self.data.addons.org_fedora_hello_world.reverse: text = text[::-1] if text: return _("Text set: %s") % text else: return _("Text not set")
SpokeWindow
widget. This widget, along with some other widgets specific to Anaconda, is found in the anaconda-widgets package. Other files required for development of add-ons with GUI support (such as Glade definitions) can be found in the anaconda-widgets-devel package.
5.6.2.2. Advanced features
pyanaconda
package contains several helper and utility functions and constructs which may be used by hubs and spokes and which have not been covered in the previous section. Most of them are located in pyanaconda.ui.gui.utils
.
englightbox
content manager which is also used in Anaconda. This manager can put a window into a lightbox to increase its visibility and focus it and to prevent users interacting with the underlying window. To demonstrate this function, the sample add-on contains a button which opens a new dialog window; the dialog itself is a special HelloWorldDialog
inheriting from the GUIObject
class, which is defined in pyanaconda.ui.gui.__init__
.
dialog
class defines the run
method which runs and destroys an internal Gtk dialog accessible through the self.window
attribute, which is populated using a mainWidgetName
class attribute with the same meaning. Therefore, the code defining the dialog is very simple, as demonstrated in the following example:
Example 13. Defining a englightbox Dialog
# every GUIObject gets ksdata in __init__ dialog = HelloWorldDialog(self.data) # show dialog above the lightbox with enlightbox(self.window, dialog.window): dialog.run()
enlightbox
context manager to run the dialog within a lightbox. The context manager needs a reference to the window of the spoke and to the dialog's window to instantiate the lightbox for them.
FirstbootSpokeMixIn
(or, more precisely, mixin) as the first inherited class defined in the pyanaconda.ui.common
module.
FirstbootOnlySpokeMixIn
class.
@gtk_action_wait
and @gtk_action_nowait
decorators), but they are out of scope of this guide. Readers are recommended to go through the installer's sources for examples.
5.6.3. Text User Interface
tui
directory as described in Section 5.5, “Anaconda Add-on Structure”.
simpleline
utility, which only allows very simple user interaction. It does not support cursor movement (instead acting like a line printer) nor any visual enhancements like using different colors or fonts.
simpleline
toolkit: App
, UIScreen
and Widget
. Widgets, which are units containing information to be shown (printed) on the screen, are placed on UIScreens which are switched by a single instance of the App
class. On top of the basic elements, there are hubs, spokes and dialogs, all containing various widgets in a way similar to the graphical interface.
NormalTUISpoke
and various other classes defined in the pyanaconda.ui.tui.spokes
package. All of those classes are based on the TUIObject
class, which itself is an equivalent of the GUIObject
class discussed in the previous chapter. Each TUI spoke is a Python class inheriting from the NormalTUISpoke
class, overriding special arguments and methods defined by the API. Because the text interface is simpler than the GUI, there are only two such arguments:
title
- determines the title of the spoke, same as thetitle
argument in the GUIcategory
- determines the category of the spoke as a string; the category name is not displayed anywhere, it is only used for groupingNote
Categories are handled differently than in the GUI. [5] It is recommended to assign a pre-existing category to your new spoke. Creating a new category would require patching Anaconda, and brings little benefit.
__init__
, initialize
, refresh
, refresh
, apply
, execute
, input
, and prompt
, and properties (ready
, completed
, mandatory
, and status
). All of these have already been described in Section 5.6.2, “Graphical user interface”.
Example 14. Defining a Simple TUI Spoke
def __init__(self, app, data, storage, payload, instclass): """ :see: pyanaconda.ui.tui.base.UIScreen :see: pyanaconda.ui.tui.base.App :param app: reference to application which is a main class for TUI screen handling, it is responsible for mainloop control and keeping track of the stack where all TUI screens are scheduled :type app: instance of pyanaconda.ui.tui.base.App :param data: data object passed to every spoke to load/store data from/to it :type data: pykickstart.base.BaseHandler :param storage: object storing storage-related information (disks, partitioning, bootloader, etc.) :type storage: blivet.Blivet :param payload: object storing packaging-related information :type payload: pyanaconda.packaging.Payload :param instclass: distribution-specific information :type instclass: pyanaconda.installclass.BaseInstallClass """ NormalTUISpoke.__init__(self, app, data, storage, payload, instclass) self._entered_text = "" def initialize(self): """ The initialize method that is called after the instance is created. The difference between __init__ and this method is that this may take a long time and thus could be called in a separated thread. :see: pyanaconda.ui.common.UIObject.initialize """ NormalTUISpoke.initialize(self) def refresh(self, args=None): """ The refresh method that is called every time the spoke is displayed. It should update the UI elements according to the contents of self.data. :see: pyanaconda.ui.common.UIObject.refresh :see: pyanaconda.ui.tui.base.UIScreen.refresh :param args: optional argument that may be used when the screen is scheduled (passed to App.switch_screen* methods) :type args: anything :return: whether this screen requests input or not :rtype: bool """ self._entered_text = self.data.addons.org_fedora_hello_world.text return True def apply(self): """ The apply method that is called when the spoke is left. It should update the contents of self.data with values set in the spoke. """ self.data.addons.org_fedora_hello_world.text = self._entered_text def execute(self): """ The excecute method that is called when the spoke is left. It is supposed to do all changes to the runtime environment according to the values set in the spoke. """ # nothing to do here pass def input(self, args, key): """ The input method that is called by the main loop on user's input. :param args: optional argument that may be used when the screen is scheduled (passed to App.switch_screen* methods) :type args: anything :param key: user's input :type key: unicode :return: if the input should not be handled here, return it, otherwise return True or False if the input was processed succesfully or not respectively :rtype: bool|unicode """ if key: self._entered_text = key # no other actions scheduled, apply changes self.apply() # close the current screen (remove it from the stack) self.close() return True def prompt(self, args=None): """ The prompt method that is called by the main loop to get the prompt for this screen. :param args: optional argument that can be passed to App.switch_screen* methods :type args: anything :return: text that should be used in the prompt for the input :rtype: unicode|None """ return _("Enter a new text or leave empty to use the old one: ")
__init__
method if it only calls the ancestor's __init__
, but the comments in the example describe the arguments passed to constructors of spoke classes in an understandable way.
initialize
method sets up a default value for the internal attribute of the spoke, which is then updated by the refresh
method and used by the apply
method to update Kickstart data. The only differences in these two methods from their equivalents in the GUI is the return type of the refresh
method (bool instead of None) and an additional args
argument they take. The meaning of the returned value is explained in the comments - it tells the application (the App
class instance) whether this spoke requires user input or not. The additional args
argument is used for passing extra information to the spoke when scheduled.
execute
method has the same purpose as the equivalent method in the GUI; in this case, the method does nothing.
input
and prompt
are specific to the text interface; there are no equivalents in Kickstart or GUI. These two methods are responsible for user interaction.
prompt
method should return a prompt which will be displayed after the content of the spoke is printed. After a string is entered in reaction to the prompt, this string is passed to the input
method for processing. The input
method then processes the entered string and takes action depending on its type and value. The above example asks for any value and then stores it as an internal attribute (key
). In more complicated add-ons, you typically need to perform some non-trivial actions, such as parse c
as "continue" or r
as "refresh", convert numbers into integers, show additional screens or toggle boolean values.
input
class must be either the INPUT_PROCESSED
or INPUT_DISCARDED
constant (both of these are defined in the pyanaconda.constants_text
module), or the input string itself (in case this input should be processed by a different screen).
apply
method is not called automatically when leaving the spoke; it must be called explicitly from the input
method. The same applies to closing (hiding) the spoke's screen, which is done by calling the close
method.
TUIObject
and call one of the self.app.switch_screen*
methods of the App
.
EditTUISpoke
class from the pyanaconda.ui.tui.spokes
package. By inheriting this class, you can implement a typical TUI spoke by only specifying fields and attributes which should be set in it. The example below demonstrates this:
Example 15. Using EditTUISpoke to Define a Text Interface Spoke
class _EditData(object): """Auxiliary class for storing data from the example EditSpoke""" def __init__(self): """Trivial constructor just defining the fields that will store data""" self.checked = False self.shown_input = "" self.hidden_input = "" class HelloWorldEditSpoke(EditTUISpoke): """Example class demonstrating usage of EditTUISpoke inheritance""" title = _("Hello World Edit") category = "localization" # simple RE used to specify we only accept a single word as a valid input _valid_input = re.compile(r'\w+') # special class attribute defining spoke's entries as: # Entry(TITLE, ATTRIBUTE, CHECKING_RE or TYPE, SHOW_FUNC or SHOW) # where: # TITLE specifies descriptive title of the entry # ATTRIBUTE specifies attribute of self.args that should be set to the # value entered by the user (may contain dots, i.e. may specify # a deep attribute) # CHECKING_RE specifies compiled RE used for deciding about # accepting/rejecting user's input # TYPE may be one of EditTUISpoke.CHECK or EditTUISpoke.PASSWORD used # instead of CHECKING_RE for simple checkboxes or password entries, # respectively # SHOW_FUNC is a function taking self and self.args and returning True or # False indicating whether the entry should be shown or not # SHOW is a boolean value that may be used instead of the SHOW_FUNC # # :see: pyanaconda.ui.tui.spokes.EditTUISpoke edit_fields = [ Entry("Simple checkbox", "checked", EditTUISpoke.CHECK, True), Entry("Always shown input", "shown_input", _valid_input, True), Entry("Conditioned input", "hidden_input", _valid_input, lambda self, args: bool(args.shown_input)), ] def __init__(self, app, data, storage, payload, instclass): EditTUISpoke.__init__(self, app, data, storage, payload, instclass) # just populate the self.args attribute to have a store for data # typically self.data or a subtree of self.data is used as self.args self.args = _EditData() @property def completed(self): # completed if user entered something non-empty to the Conditioned input return bool(self.args.hidden_input) @property def status(self): return "Hidden input %s" % ("entered" if self.args.hidden_input else "not entered") def apply(self): # nothing needed here, values are set in the self.args tree pass
_EditData
serves as a data container which is used to store values entered by the user. The HelloWorldEditSpoke
class defines a simple spoke with one checkbox and two entries, all of which are instances of the EditTUISpokeEntry
class imported as the Entry
class). The first one is shown every time the spoke is displayed, the second instance is only shown if the first one contains a non-empty value.
EditTUISpoke
class, see the comments in the above example.
5.7. Deploying and testing an Anaconda add-on
/usr/share/anaconda/addons/
directory in the installation runtime environment; to add your own add-on into that directory, you must create a product.img
file with the same directory structure and place it on your boot media.
product.img
file and repackaging the image, see Section 2, “Working with ISO Images”.
inst.kdump_addon=on
option in the boot menu.