Chapter 5. Developing installer add-ons
This section provides details about Anaconda and it’s architecture, and how to develop your own add-ons. The details about Anaconda and its architecture helps you to understand Anaconda backend and various plug points for the add-ons to work. It also helps to accordingly develop the add-ons.
5.1. Introduction to Anaconda and add-ons
Anaconda is the operating system installer used in Fedora, Red Hat Enterprise Linux, and their derivatives. It is a set of Python modules and scripts together with some additional files like 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 up this system on a machine. The installation process has four major steps:
- Prepare installation destination (usually disk partitioning)
- Install package and data
- Install and configure boot loader
- Configure newly installed system
Using Anaconda enables you to install Fedora, Red Hat Enterprise Linux, and their derivatives, in the following three ways:
Using graphical user interface (GUI):
This is the most common installation method. The interface allows users to install the system interactively with little or no configuration required before starting the installation. This method covers all common use cases, including setting up complicated partitioning layouts.
The graphical interface supports remote access over VNC
, which allows you to use the GUI even on systems with no graphics cards or attached monitor.
Using text user interface (TUI):
The TUI works similar to a monochrome line printer, which allows it to work on serial consoles that do not support cursor movement, colors and other advanced features. The text mode is limited and allows you to customize only the 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.
Using Kickstart file:
A Kickstart file is a plain text file with shell-like syntax that can contain data to drive the installation process. A Kickstart file allows you to partially or completely automate the installation. A set of commands which configures all required areas is necessary to completely automate the installation. If one or more commands are missed, the installation requires interaction.
Apart from automation of the installer itself, Kickstart files can contain custom scripts that are run at specific moments during the installation process.
5.2. Anaconda Architecture
Anaconda is a set of Python modules and scripts. It also uses several external packages and libraries. The major components of this toolset include the following packages:
-
pykickstart
- parses and validates the Kickstart files. Also, provides data structure that stores values that drive the installation. -
yum
- the package manager that installs packages and resolves dependencies -
blivet
- handles all activities related to storage management -
pyanaconda
- contains the user interface and modules for Anaconda, such as keyboard and timezone selection, network configuration, and user creation. Also provides various utilities to perform system-oriented functions -
python-meh
- contains an exception handler that 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 -
dasbus
- enables communication between theD-Bus
library with modules of anaconda and with external components -
python-simpleline
- text UI framework library to manage user interaction in the Anaconda text mode -
gtk
- the Gnome toolkit library for creating and managing GUI
Apart from the division into packages previously mentioned, Anaconda is internally divided into the user interface and a set of modules that run as separate processes and communicate using the D-Bus
library. These modules are:
-
Boss
- manages the internal module discovery, lifecycle, and coordination -
Localization
- manages locales -
Network
- handles network -
Payloads
- handles data for installation in different formats, such asrpm
,ostree
,tar
and other installation formats. Payloads manage the sources of data for installation; sources can vary in format such as CD-ROM, HDD, NFS, URLs, and other sources -
Security
- manages security related aspects -
Services
- handles services -
Storage
- manages storage usingblivet
-
Subscription
- handles thesubscription-manager
tool and Insights. -
Timezone
- deals with time, date, zones, and time synchronization. -
Users
- creates users and groups.
Each module declares which parts of Kickstart it handles, and has methods to apply the configuration from Kickstart to the installation environment and to the installed system.
The Python code portion of Anaconda (pyanaconda
) starts as a “main” process that owns the user interface. Any Kickstart data you provide are parsed using the pykickstart
module and the Boss
module is started, it discovers all other modules, and starts them. Main process then sends Kickstart data to the modules according to their declared capabilities. Modules process the data, apply the configuration to the installation environment, and the UI validates if all required choices have been made. If not, you must supply the data in an interactive installation mode. Once all required choices have been made, the installation can start - the modules write data to the installed system.
5.3. Anaconda user interface
The Anaconda user interface (UI) has a non-linear structure, also known as hub and spoke model.
The advantages of Anaconda hub and spoke model are:
- Flexibility to follow the installer screens.
- Flexibility to retain the default settings.
- Provides an overview of the configured values.
- Supports extensibility. You can add hubs without the need to reorder anything and can resolve some complex ordering dependencies.
- Supports installation in graphical and text mode.
The following diagram shows the installer layout and the possible interactions between hubs and spokes (screens):
Figure 5.1. Hub and spoke model
In the diagram, screens 2-13 are called normal spokes, and screens 1 and 14 are standalone spokes. Standalone spokes are the screens that can be used before or after the standalone spoke or hub. For example, the Welcome
screen at the beginning of the installation which prompts you to choose your language for the rest of the installation.
-
The
Installation Summary
is the only hub in Anaconda. It shows a summary of configured options before the installation begins
Each spoke has the following predefined properties that reflect the hub.
-
ready
- states whether or not you can visit a spoke. For example, when the installer is configuring a package source, the spoke is colored in gray, and you cannot access it until the configuration is complete. -
completed
- marks whether or not the spoke is complete (all required values are set). -
mandatory
- determines whether you must visit the spoke before continuing the installation; for example, you must visit theInstallation Destination
spoke, 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)
To make the user interface clearer, spokes are grouped together into categories. For example, the Localization
category groups together spokes for keyboard layout selection, language support and time zone settings.
Each spoke contains UI controls which display and allow you to modify values from one or more modules. The same applies to spokes that add-ons provide.
5.4. Communication across Anaconda threads
Some of the actions that you need to perform during the installation process may take a long time. For example, scanning disks for existing partitions or downloading package metadata. To prevent you from waiting and remaining responsive, Anaconda runs these actions in separate threads.
The Gtk toolkit does not support element changes from multiple threads. The main event loop of Gtk runs in the main thread of the Anaconda process. Therefore, all actions pertaining to the GUI must be performed in the main thread. To do so, use GLib.idle_add
, which is not always easy or desired. Several helper functions and decorators that are defined in the pyanaconda.ui.gui.utils module may add to the difficulty.
The @gtk_action_wait
and @gtk_action_nowait
decorators 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 that runs in the main thread. The return value is either returned to the caller or dropped, respectively.
In a spoke and hub communication, a spoke announces when it is ready and is not blocked. The hubQ
message queue handles this function, and periodically checks the main event loop. When a spoke becomes accessible, it sends a message to the queue announcing the change and that it should no longer be blocked.
The same applies in a situation where a spoke needs to refresh its status or complete a flag. The Configuration and Progress
hub has a different queue called progressQ
which serves as a medium to transfer installation progress updates.
These mechanisms are also used for the text-based interface. In the text mode, there is no main loop, but the keyboard input takes most of the time.
5.5. Anaconda modules and D-Bus library
Anaconda’s modules run as independent processes. To communicate with these processes via their D-Bus
API, use the dasbus
library.
Calls to methods via D-Bus
API are asynchronous, but with the dasbus
library you can convert them to synchronous method calls in Python. You can also write either of the following programs:
- program with asynchronous calls and return handlers
- A program with synchronous calls that makes the caller wait until the call is complete.
For more information about threads and communication, see Communication across Anaconda threads.
Additionally, Anaconda uses Task objects running in modules. Tasks have a D-Bus
API and methods that are automatically executed in additional threads. To successfully run the tasks, use the sync_run_task
and async_run_task
helper functions.
5.6. The Hello World addon example
Anaconda developers publish an example addon called “Hello World”, available on GitHub: https://github.com/rhinstaller/hello-world-anaconda-addon/ The descriptions in further sections are reproduced in this.
5.7. Anaconda add-on structure
An Anaconda add-on is a Python package that contains a directory with an __init__.py
and other source directories (subpackages). Because Python allows you to import each package name only once, specify a unique name for the package top-level directory. You can use an arbitrary name, because add-ons are loaded regardless of their name - the only requirement is that they must be placed in a specific directory.
The suggested naming convention for add-ons is similar to Java packages or D-Bus service names.
To make the directory name a unique identifier for a Python package, prefix the add-on name with the reversed domain name of your organization, using underscores (_
) instead of dots. For example, com_example_hello_world
.
Make sure to create an __init__.py
file in each directory. Directories missing this file are considered as invalid Python packages.
When writing an add-on, ensure the following:
-
Support for each interface (graphical interface and text interface) is available in a separate subpackage and these subpackages are named
gui
for the graphical interface andtui
for the text-based interface. -
The
gui
andtui
packages contain aspokes
subpackage. [1] - Modules contained in the packages have an arbitrary name.
-
The
gui/
andtui/
directories contain Python modules with any name. - There is a service that performs the actual work of the addon. This service can be written in Python or any other language.
- The service implements support for D-Bus and Kickstart.
- The addon contains files that enable automatic startup of the service.
Following is a sample directory structure for an add-on which supports every interface (Kickstart, GUI and TUI):
Example 5.1. Sample add-on structure
com_example_hello_world ├─ gui │ ├─ init.py │ └─ spokes │ └─ init.py └─ tui ├─ init.py └─ spokes └─ init.py
Each package must contain at least one module with an arbitrary name defining the classes that are inherited from one or more classes defined in the API.
For all add-ons, follow Python’s PEP 8 and PEP 257 guidelines for docstring conventions. There is no consensus on the format of the actual content of docstrings in Anaconda; the only requirement is that they are human-readable. If you plan to use auto-generated documentation for your add-on, docstrings should follow the guidelines for the toolkit you use to accomplish this.
You can include a category subpackage if an add-on needs to define a new category, but this is not recommended.
5.8. Anaconda services and configuration files
Anaconda services and configuration files are included in data/ directory. These files are required to start the add-ons service and to configure D-Bus.
Following are some examples of Anaconda Hello World add-on:
Example 5.2. Example of addon-name.conf:
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> <busconfig> <policy user="root"> <allow own="org.fedoraproject.Anaconda.Addons.HelloWorld"/> <allow send_destination="org.fedoraproject.Anaconda.Addons.HelloWorld"/> </policy> <policy context="default"> <deny own="org.fedoraproject.Anaconda.Addons.HelloWorld"/> <allow send_destination="org.fedoraproject.Anaconda.Addons.HelloWorld"/> </policy> </busconfig>
This file must be placed in the /usr/share/anaconda/dbus/confs/
directory in the installation environment. The string org.fedoraproject.Anaconda.Addons.HelloWorld
must correspond to the location of addon’s service on D-Bus.
Example 5.3. Example of addon-name.service:
[D-BUS Service]
# Start the org.fedoraproject.Anaconda.Addons.HelloWorld service.
# Runs org_fedora_hello_world/service/main.py
Name=org.fedoraproject.Anaconda.Addons.HelloWorld
Exec=/usr/libexec/anaconda/start-module org_fedora_hello_world.service
User=root
This file must be placed in the /usr/share/anaconda/dbus/services/
directory in the installation environment. The string org.fedoraproject.Anaconda.Addons.HelloWorld
must correspond to the location of addon’s service on D-Bus. The value on the line starting with Exec=
must be a valid command that starts the service in the installation environment.
5.9. GUI Add-on basic features
Similarly to Kickstart support in add-ons, GUI support requires that every part of the add-on must contain at least one module with a definition of a class inherited from a particular class defined by the API. For the graphical add-on support, the only class you should add is the NormalSpoke
class, defined in pyanaconda.ui.gui.spokes
, as a class for the normal spoke type of screen. To learn more about it, see Anaconda user interface.
To implement a new class inherited from NormalSpoke
, you must define the following class attributes that the API requires:
-
builderObjects
- lists all top-level objects from the spoke’s.glade
file that should be exposed to the spoke with their children objects (recursively). In case everything should be exposed to the spoke, which is not recommended, the list should be empty. -
mainWidgetName
- contains the id of the main window widget (Add Link) as defined in the.glade
file. -
uiFile
- contains the name of the.glade
file. -
category
- contains the class of the category the spoke belongs to. -
icon
- contains the identifier of the icon that will be used for the spoke on the hub. -
title
- defines the title that will be used for the spoke on the hub.
5.10. Adding support for the Add-on graphical user interface (GUI)
This section describes how to add support to the graphical user interface (GUI) of your add-on by performing the following high-level steps:
- Define Attributes Required for the Normalspoke Class
-
Define the
__init__
andinitialize
Methods -
Define the
refresh
,apply
, andexecute
Methods -
Define the
status
and theready
,completed
andmandatory
Properties
Prerequisites
- Your add-on includes support for Kickstart. See Anaconda add-on structure.
-
Install the anaconda-widgets and anaconda-widgets-devel packages, which contain Gtk widgets specific for
Anaconda
, such asSpokeWindow
.
Procedure
- Create the following modules with all required definitions to add support for the Add-on graphical user interface (GUI), according to the following examples.
Example 5.4. 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(FirstbootSpokeMixIn, 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. Since it is also inherited form the FirstbootSpokeMixIn, it will also appear in the Initial Setup (successor of the Firstboot tool). :see: pyanaconda.ui.common.UIObject :see: pyanaconda.ui.common.Spoke :see: pyanaconda.ui.gui.GUIObject :see: pyanaconda.ui.common.FirstbootSpokeMixIn :see: pyanaconda.ui.gui.spokes.NormalSpoke """ # 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")
The __all__
attribute exports the spoke
class, followed by the first lines of its definition including definitions of attributes previously mentioned in GUI Add-on basic features. These attribute values are referencing widgets defined in the com_example_hello_world/gui/spokes/hello.glade
file. Two other notable attributes are present:
-
category
, which has its value imported from theHelloWorldCategory
class from thecom_example_hello_world.gui.categories
module. TheHelloWorldCategory
that the path to add-ons is insys.path
so that values can be imported from thecom_example_hello_world
package. Thecategory
attribute is part of theN_ function
name, which marks the string for translation; but returns the non-translated version of the string, as the translation happens in a later stage. -
title
, which contains one underscore in its definition. Thetitle
attribute underscore marks the beginning of the title itself and makes the spoke reachable by using theAlt+H
keyboard shortcut.
What usually follows the header of the class definition and the class attributes
definitions is the constructor that initializes an instance of the class. In case of the Anaconda graphical interface objects, there are two methods initializing a new instance: the __init__
method and the initialize
method.
The reason behind two such functions is that the GUI objects may be created in memory at one time and fully initialized at a different time, as the spoke
initialization could be time consuming. Therefore, the __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.
In the Hello World add-on
example, define these two methods as follows. Note the number and description of the arguments passed to the __init__
method.
Example 5.5. Defining the __init__
and initialize Methods:
def __init__(self, data, storage, payload): """ :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 """ NormalSpoke.init(self, data, storage, payload) self._hello_world_module = HELLO_WORLD.get_proxy() 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 separate thread. :see: pyanaconda.ui.common.UIObject.initialize """ NormalSpoke.initialize(self) self._entry = self.builder.get_object("textLines") self._reverse = self.builder.get_object("reverseCheckButton")
The data parameter passed to the __init__
method 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.
The storage object
is no longer usable as of RHEL8. If your add-on needs to interact with storage configuration, use the Storage DBus
module.
Because the HelloWorldData class has already been defined in The Hello World addon example, there already is a subtree in self.data for this add-on. Its root, an instance of the class, is available as self.data.addons.com_example_hello_world
.
Another action that an ancestor’s __init__
does is initializing an instance of the GtkBuilder with the spoke’s .glade
file and storing it as self.builder
. The initialize
method uses this to get the GtkTextEntry
used to show and modify the text from the kickstart file’s %addon section.
The __init__
and initialize
methods are both important when the spoke is created. However, the main role of the spoke is to be visited by a user who wants to change or review the spoke’s values 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 ensure that the displayed data matches internal data structures and, with that, to ensure that current values stored in the self.data structure are displayed. -
apply
- called when the spoke is left and used to store values from UI elements back into theself.data
structure. -
execute
- called when users leave the spoke and used to perform any runtime changes based on the new state of the spoke.
These functions are implemented in the sample Hello World add-on in the following way:
Example 5.6. 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 internal data structures. :see: pyanaconda.ui.common.UIObject.refresh """ lines = self._hello_world_module.Lines self._entry.get_buffer().set_text("".join(lines)) reverse = self._hello_world_module.Reverse self._reverse.set_active(reverse) def apply(self): """ The apply method that is called when user leaves the spoke. It should update the D-Bus service with values set in the GUI elements. """ buf = self._entry.get_buffer() text = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), True) lines = text.splitlines(True) self._hello_world_module.SetLines(lines) self._hello_world_module.SetReverse(self._reverse.get_active()) def execute(self): """ The execute method that is called when the spoke is exited. 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
You can use several additional methods to control the spoke’s state:
-
ready
- determines whether the spoke is ready to be visited; if the value is "False", thespoke
is not accessible, for example, thePackage Selection
spoke before a package source is configured. -
completed
- determines if the spoke has been completed. -
mandatory
- determines if the spoke is mandatory or not, for example, theInstallation Destination
spoke, which must always be visited, even if you want to use automatic partitioning.
All of these attributes need to be dynamically determined based on the current state of the installation process.
Below is a sample implementation of these methods in the Hello World add-on, which requires a certain value to be set in the text attribute of the HelloWorldData
class:
Example 5.7. Defining the ready, completed and mandatory Methods
@property
def ready(self):
"""
The ready property
reports whether the spoke is ready, that is, can be visited
or not. The spoke is made (in)sensitive based on the returned value of the ready
property.
:rtype: bool
"""
# this spoke is always ready
return True
@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
After these properties are defined, the spoke can control its accessibility and completeness, but it cannot provide a summary of the values configured within - you must visit the spoke to see how it is configured, which may not be desired. For this reason, an additional property called 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.
The status property is defined in the Hello World
example add-on as follows:
Example 5.8. 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 """ lines = self._hello_world_module.Lines if not lines: return _("No text added") elif self._hello_world_module.Reverse: return _("Text set with {} lines to reverse").format(len(lines)) else: return _("Text set with {} lines").format(len(lines))
After defining all properties described in the examples, the add-on has full support for showing a graphical user interface (GUI) as well as Kickstart.
The example demonstrated here is very simple and does not contain any controls; knowledge of Python Gtk programming is required to develop a functional, interactive spoke in the GUI.
One notable restriction is that each spoke must have its own main window - an instance of the SpokeWindow
widget. This widget, along with other widgets specific to Anaconda, is found in the anaconda-widgets
package. You can find other files required for development of add-ons with GUI support, such as Glade
definitions, in the anaconda-widgets-devel
package.
Once your graphical interface support module contains all necessary methods you can continue with the following section to add support for the text-based user interface, or you can continue with Deploying and testing an Anaconda add-on and test the add-on.
5.11. Add-on GUI advanced features
The pyanaconda
package contains several helper and utility functions, as well as constructs which may be used by hubs and spokes. Most of them are located in the pyanaconda.ui.gui.utils
package.
The sample Hello World
add-on demonstrates usage of the englightbox
content manager which Anaconda also uses. This content manager can put a window into a lightbox to increase its visibility and focus it 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.
The dialog class defines the run method that 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 5.9. Defining a englightbox Dialog
# every GUIObject gets ksdata in init
dialog = HelloWorldDialog(self.data)
# show dialog above the lightbox
with self.main_window.enlightbox(dialog.window):
dialog.run()
The Defining an englightbox Dialog
example code creates an instance of the dialog and then uses the enlightbox context manager to run the dialog within a lightbox. The context manager has a reference to the window of the spoke and only needs the dialog’s window to instantiate the lightbox for the dialog.
Another useful feature provided by Anaconda is the ability to define a spoke that will appear both during the installation and after the first reboot. The Initial Setup
utility is described in Adding support for the Add-on graphical user interface (GUI). To make a spoke available in both Anaconda and Initial Setup, it must inherit the special FirstbootSpokeMixIn
class, also known as mixin
, as the first inherited class defined in the pyanaconda.ui.common
module.
To make a spoke available in Anaconda and the reconfiguration mode of the Initial Setup, it must inherit the special FirstbootSpokeMixIn
class, also known as mixin
, as the first inherited class defined in the pyanaconda.ui.common
module.
If you want to make a certain spoke available only in Initial Setup, this spoke should instead inherit the FirstbootOnlySpokeMixIn
class.
To make a spoke always available in both Anaconda and Initial Setup, the spoke should redefine the should_run
method, as demonstrated in the following example:
Example 5.10. Redefining the should_run method
@classmethod def should_run(cls, environment, data): """Run this spoke for Anaconda and Initial Setup""" return True
The pyanaconda
package provides many more advanced features, such as the @gtk_action_wait
and @gtk_action_nowait
decorators, but they are out of scope of this guide. For more examples, refer to the installer’s sources.
5.12. TUI Add-on basic features
Anaconda also supports a text-based interface (TUI). This interface is more limited in its capabilities, but on some systems it might be the only choice for an interactive installation. For more information about differences between the text-based interface and graphical interface and about limitations of the TUI, see Introduction to Anaconda and add-ons.
To add support for the text interface into your add-on, create a new set of subpackages under the tui directory as described in Anaconda add-on structure.
The text mode support in the installer is based on the simpleline
library, which only allows very simple user interaction. The text mode interface:
- Does not support cursor movement - instead, it acts like a line printer.
- Does not support any visual enhancements, such as using different colors or fonts, for example.
Internally, the simpleline
toolkit has three main classes: App
, UIScreen
and Widget
. Widgets are units containing information to be printed on the screen. They are placed on UIScreens that are switched by a single instance of the App class. On top of the basic elements, hubs
, spoke`s and `dialogs
all contain various widgets in a way similar to the graphical interface.
The most important classes for an add-on are NormalTUISpoke
and various other classes defined in the pyanaconda.ui.tui.spokes
package. All those classes are based on the TUIObject
class, which itself is an equivalent of the GUIObject
class discussed in Add-on GUI advanced features. 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, similar as the title argument in the GUI. -
category
- determines the category of the spoke as a string; the category name is not displayed anywhere, it is only used for grouping.
The TUI handles categories differently than the GUI. 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.
Each spoke is also expected to override several methods, namely init
, initialize
, refresh
, refresh
, apply
, execute
, input
, prompt
, and properties
(ready
, completed
, mandatory
, and status
).
Additional resources
5.13. Defining a Simple TUI Spoke
The following example shows the implementation of a simple Text User Interface (TUI) spoke in the Hello World sample add-on:
Prerequisites
- You have created a new set of subpackages under the tui directory as described in Anaconda add-on structure.
Procedure
- Create modules with all required definitions to add support for the add-on text user interface (TUI), according to the following examples:
Example 5.11. Defining a Simple TUI Spoke
def __init__(self, *args, **kwargs): """ Create the representation of the spoke. :see: simpleline.render.screen.UIScreen """ super().__init__(*args, **kwargs) self.title = N_("Hello World") self._hello_world_module = HELLO_WORLD.get_proxy() self._container = None self._reverse = False self._lines = "" 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 """ # nothing to do here super().initialize() def setup(self, args=None): """ The setup method that is called right before the spoke is entered. It should update its state according to the contents of DBus modules. :see: simpleline.render.screen.UIScreen.setup """ super().setup(args) self._reverse = self._hello_world_module.Reverse self._lines = self._hello_world_module.Lines return True def refresh(self, args=None): """ The refresh method that is called every time the spoke is displayed. It should generate the UI elements according to its state. :see: pyanaconda.ui.common.UIObject.refresh :see: simpleline.render.screen.UIScreen.refresh """ super().refresh(args) self._container = ListColumnContainer( columns=1 ) self._container.add( CheckboxWidget( title="Reverse", completed=self._reverse ), callback=self._change_reverse ) self._container.add( EntryWidget( title="Hello world text", value="".join(self._lines) ), callback=self._change_lines ) self.window.add_with_separator(self._container) def _change_reverse(self, data): """ Callback when user wants to switch checkbox. Flip state of the "reverse" parameter which is boolean. """ self._reverse = not self._reverse def _change_lines(self, data): """ Callback when user wants to input new lines. Show a dialog and save the provided lines. """ dialog = Dialog("Lines") result = dialog.run() self._lines = result.splitlines(True) def input(self, args, key): """ The input method that is called by the main loop on user's input. * If the input should not be handled here, return it. * If the input is invalid, return InputState.DISCARDED. * If the input is handled and the current screen should be refreshed, return InputState.PROCESSED_AND_REDRAW. * If the input is handled and the current screen should be closed, return InputState.PROCESSED_AND_CLOSE. :see: simpleline.render.screen.UIScreen.input """ if self._container.process_user_input(key): return InputState.PROCESSED_AND_REDRAW if key.lower() == Prompt.CONTINUE: self.apply() self.execute() return InputState.PROCESSED_AND_CLOSE return super().input(args, key) def apply(self): """ The apply method is not called automatically for TUI. It should be called in input() if required. It should update the contents of internal data structures with values set in the spoke. """ self._hello_world_module.SetReverse(self._reverse) self._hello_world_module.SetLines(self._lines) def execute(self): """ The execute method is not called automatically for TUI. It should be called in input() if required. It is supposed to do all changes to the runtime environment according to the values set in the spoke. """ # nothing to do here pass
It is not necessary to override the 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.
In the previous example:
-
The
setup
method sets up a default value for the internal attribute of the spoke on every entry, which is then displayed by therefresh
method, updated by theinput
method and used by theapply
method to update internal data structures. -
The
execute
method has the same purpose as the equivalent method in the GUI; in this case, the method has no effect. -
The
input
method is specific to the text interface; there are no equivalents in Kickstart or GUI. Theinput
methods are responsible for user interaction. -
The
input
method 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 complex add-ons, you typically need to perform some non-trivial actions, such as parse letters as actions, convert numbers into integers, show additional screens or toggle boolean values. -
The
return
value of the input class must be either theInputState
enum or theinput
string itself, in case this input should be processed by a different screen. In contrast to the graphical mode, theapply
andexecute
methods are not called automatically when leaving the spoke; they must be called explicitly from the input method. The same applies to closing (hiding) the spoke’s screen: it must be called explicitly from theclose
method.
To show another screen, for example if you need additional information that was entered in a different spoke, you can instantiate another TUIObject
and use ScreenHandler.push_screen_modal()
to show it.
Due to restrictions of the text-based interface, TUI spokes tend to have a very similar structure, that consists of a list of checkboxes or entries that should be checked or unchecked and populated by the user.
5.14. Using NormalTUISpoke to Define a Text Interface Spoke
The Defining a Simple TUI Spoke example showed a way to implement a TUI spoke where its methods handle printing and processing the available and provided data. However, there is a different way to accomplish this using the Normal 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 that should be set in it. The following example demonstrates this:
Prerequisites
-
You have added a new set of subpackages under the
TUI
directory, as described in Anaconda add-on structure.
Procedure
- Create modules with all required definitions to add support for the Add-on text user interface (TUI), according to the following examples.
Example 5.12. Using NormalTUISpoke to Define a Text Interface Spoke
class HelloWorldEditSpoke(NormalTUISpoke): """Example class demonstrating usage of editing in TUI""" category = HelloWorldCategory def init(self, data, storage, payload): """ :see: simpleline.render.screen.UIScreen :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 """ NormalTUISpoke.init(self, data, storage, payload) self.title = N_("Hello World Edit") self._container = None # values for user to set self._checked = False self._unconditional_input = "" self._conditional_input = "" 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: simpleline.render.screen.UIScreen.refresh :param args: optional argument that may be used when the screen is scheduled :type args: anything """ super().refresh(args) self._container = ListColumnContainer(columns=1) # add ListColumnContainer to window (main window container) # this will automatically add numbering and will call callbacks when required self.window.add(self._container) self._container.add(CheckboxWidget(title="Simple checkbox", completed=self._checked), callback=self._checkbox_called) self._container.add(EntryWidget(title="Unconditional text input", value=self._unconditional_input), callback=self._get_unconditional_input) # show conditional input only if the checkbox is checked if self._checked: self._container.add(EntryWidget(title="Conditional password input", value="Password set" if self._conditional_input else ""), callback=self._get_conditional_input) self._window.add_separator() @property def completed(self): # completed if user entered something non-empty to the Conditioned input return bool(self._conditional_input) @property def status(self): return "Hidden input %s" % ("entered" if self._conditional_input else "not entered") def apply(self): # nothing needed here, values are set in the self.args tree pass
5.15. Deploying and testing an Anaconda add-on
You can deploy and test your own Anaconda add-on into the installation environment. To do so, follow the steps:
Prerequisites
- You created an Add-on.
-
You have access to your
D-Bus
files.
Procedure
-
Create a directory
DIR
at the place of your preference. -
Add the
Add-on
python files intoDIR/usr/share/anaconda/addons/
. -
Copy your
D-Bus
service file intoDIR/usr/share/anaconda/dbus/services/
. -
Copy your
D-Bus
service configuration file to/usr/share/anaconda/dbus/confs/
. Create the updates image.
Access the
DIR
directory:cd DIR
Locate the updates image.
find . | cpio -c -o | pigz -9cv > DIR/updates.img
- Extract the contents of the ISO boot image.
Use the resulting
updates
image:-
Add the
updates.img
file into the images directory of your unpacked ISO contents. - Repack the image.
-
Set up a web server to provide the
updates
.img file to the Anaconda installer via HTTP. Load
updates
.img file at boot time by adding the following specification to the boot options.inst.updates=http://your-server/whatever/updates.img to boot options.
-
Add the
For specific instructions on unpacking an existing boot image, creating a product.img
file and repackaging the image, see Extracting Red Hat Enterprise Linux boot images.