Product SiteDocumentation Site

Fedora 20

Anaconda Addon Development Guide

Things one needs to know when writing an Anaconda installer Addon

Vratislav Podzimek

Legal Notice

Copyright © 2013 Fedora Project Contributors.
The text of and illustrations in this document are licensed by Red Hat under a Creative Commons Attribution–Share Alike 3.0 Unported license ("CC-BY-SA"). An explanation of CC-BY-SA is available at http://creativecommons.org/licenses/by-sa/3.0/. The original authors of this document, and Red Hat, designate the Fedora Project as the "Attribution Party" for purposes of CC-BY-SA. In accordance with CC-BY-SA, if you distribute this document or an adaptation of it, you must provide the URL for the original version.
Red Hat, as the licensor of this document, waives the right to enforce, and agrees not to assert, Section 4d of CC-BY-SA to the fullest extent permitted by applicable law.
Red Hat, Red Hat Enterprise Linux, the Shadowman logo, JBoss, MetaMatrix, Fedora, the Infinity Logo, and RHCE are trademarks of Red Hat, Inc., registered in the United States and other countries.
For guidelines on the permitted uses of the Fedora trademarks, refer to https://fedoraproject.org/wiki/Legal:Trademark_guidelines.
Linux® is the registered trademark of Linus Torvalds in the United States and other countries.
Java® is a registered trademark of Oracle and/or its affiliates.
XFS® is a trademark of Silicon Graphics International Corp. or its subsidiaries in the United States and/or other countries.
MySQL® is a registered trademark of MySQL AB in the United States, the European Union and other countries.
All other trademarks are the property of their respective owners.

Abstract

The Anaconda installer is a piece of software used to install Fedora, RHEL and their derivatives. Since the installation is internally a complicated process and there are many aspects of the resulting system that need to be configured during installation it has been decided that the Anaconda installer needs to support dynamically loaded plugins (addons) that will be developed and maintained by other teams and people specialized in certain areas. This guide should help those people writting an Anaconda addon to get an insight to the architecture and basic principles used by the Anaconda installer as well as an overview of the API and helper functions provided.
1. Introduction
2. Architecture
3. Hub&Spoke model
4. Threads and communication
5. Addon structure
6. Writing an Anaconda addon
6.1. Kickstart support
6.2. Graphical user interface
6.3. Textual user interface
7. Deploying and testing an Anaconda addon
8. Addons welcomed
A. Revision History
Index

1. Introduction

Anaconda is the operating system installer (OS) used in Fedora, RHEL and their derivatives. At a closer look, 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. Altogether 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 final 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
There are three ways the user can set parameters for the target system (and in some cases also for the installation process). The most commonly used is via the graphical user interface (GUI) which should cover all common use cases and should be clear and easily understandable even for non-advanced users. Although Anaconda also supports installation over VNC , there are some corner cases where a textual interface is needed, such as installation over serial console on "exotic" pieces of hardware. For this reason, Anaconda also has a textual user interface (TUI) that works the same way as a black-only line printer. This behavior was chosen as a result of various serial consoles not supporting cursor movement, colors and other "advanced" features. Text mode installation implements only the most important features of the graphical installation and usually needs to be combined with installer-specific command line arguments since it does not provide all of the options the GUI provides. The third and most advanced way to set installation parameters is by using a kickstart file. This is a simple file with shell-like syntax which can contain data driving the installation process, which then runs automatically. If the kickstart file doesn't contain all data required, the installer asks the user about the missing pieces. More information about kickstart can be found at the Anaconda/Kickstart wiki page. Addons related kickstart specifications are covered in Section 5, “Addon structure”. The important distinction to note is that, compared to the TUI, which is not a full-featured mode of installation, kickstart installation provides the highest number of configuration options. The golden rule is that everything has to be supported in kickstart first. Then the GUI and TUI pieces can follow, supporting subsets of configuration options provided in kickstart that allow the user interface (UI) to remain clear and succinct. Anaconda has to maintain a balance between simplicity and complexity which is difficult to achieve.
It should be clear by now that there are many things which need to happen during the installation process. Many teams and individuals favor the idea of adding more options to the installer so the target system can be pre-configured before it is run for the first time. On one hand, the installation environment is quite specific, and commonly used tools utilized by Anaconda are not always ready to work in it. On the other hand, once a feature is supported via kickstart, multiple systems with the same configuration can be installed easily. However, the more code in the installer, the more bugs it contains. Moreover, the maintenance becomes increasingly difficult with additional functionality, which makes it difficult to follow changes in so many areas. For these reasons, it was decided when doing a major rewrite of the Anaconda installer, support for addons must be added. This allows other teams and individuals to participate and contribute as experts in their particular areas of interest by developing and maintaining their own pieces of code. This guide is intended to serve as a tool for lowering the barrier to entry by providing an understanding of how the Anaconda installer works and looks like internally, as well as explaining how its functionality can be extended with an addon.
Traditionally, the first boot of the newly installed system is also considered to be a part of the installation procedure--usually, some configuration of the system (e.g. user creation) is taking place at this point. For many years there has been a graphical tool called Firstboot , which has helped users configure some aspects of the newly installed system. The issue with Firstboot is that it is using Gtk2 and the pygtk module, which are both no longer being developed. Since the goal was to provide all existing options from Firstboot during the installation process, it was obvious that the rewritten Firstboot should be sharing code with the Anaconda installer. [1] That is how a new tool called Initial Setup was born. It reuses a lot of code from the installer: it has the same model and design as anaconda, and it reads data produced by the installer to show which values are set during the installation process. Most noteworthy is that only a little is needed for an Anaconda addon to become an Initial Setup addon. This is explained in more detail in Section 6, “Writing an Anaconda addon”.
Some additional information about the Anaconda installer can be found at the Anaconda wiki page, and additional information about the rewrite can be found at the Anaconda/NewInstaller wiki page.

2. Architecture

As already noted, the Anaconda installer is a set of python modules and scripts. It also uses a number of external packages and libraries, some of which were originally created specifically for the installer. Major components of the toolset include the following packages: pykickstart, yum, blivet and pyanaconda. pykickstart is a package that parses and validates a provided kickstart file and also provides a data structure that Anaconda uses to store values driving installation. We will focus more on the data representation and life cycle in the following paragraph. yum is the core python package of the yum package manager. Anaconda uses it to interface with package repositories and handle operations related to package management during installation. blivet is a relatively recent project which was split out from the pyanaconda package as pyanaconda.storage. As the old name suggests, blivet is a storage library which handles all activities related to disk management. Additionally, it provides functions for boot loader installation and configuration. The pyanaconda package acts as a glue for holding all of the components together; it contains all of the UI code and several 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 functions. There is also the python-meh package which contains an exception handler that gathers and stores additional information from the system in case of a crash and passes this data to the libreport library, which is a part of the ABRT project.
The life cycle of the data in the installation process is simple and straightforward. If a kickstart file is provided, it is parsed and processed by the pykickstart module to behave as an in-memory, tree-like structure. If no kickstart file is given, an empty tree-like structure is created. Items in this structure are then updated with the user's choices as they are made within the UI. Next, the installation process begins, and it is driven by the values which are stored in the tree-like structure. At this point, the values are also written out as a kickstart file which can be used to perform another installation with the same configuration. Elements of the structure are defined in the pykickstart package, but some of them are overridden by modified versions from the pyanaconda.kickstart module. The important rule which governs this behavior is that there is nowhere to store configuration data, and the installation process is data-driven and relies on transactions as much as possible. This neatly enforces some nice features:
  • everything has to be supported in kickstart first, because in an automated installation only the transaction happens
  • there is a single and obvious point where the underlying machine starts to be modified -- this marks the beginning of the transaction
  • everything configured in the UI is reflected in the resultant kickstart file
What does it mean that the installation is data-driven? The installation and configuration logic lies within the methods of the items in the tree-like structure. Every item is set up (the setup method) to modify the runtime environment of the installation if needed, and then executed (the execute) to perform the changes on the newly installed system. We will look at these methods closer in Section 6, “Writing an Anaconda addon”.

3. Hub&Spoke model

One of the biggest changes introduced with the rewrite of the Anaconda installer was that the new UI (both graphical and textual) uses a hub&spoke model instead of the wizard model that had been used before. The following diagram shows how hub&spoke works regarding the possible transitions between screens (hubs and spokes).

Figure 1. Hub&spoke model diagram

The screens 2, 3, 5, 8, 10 and others are examples of so called normal spokes. Those are the screens reachable from hubs. The screens 1 and 14 are examples of a so called standalone spoke which is a type of screen that should be used only in case it has to be visited before (after) the following (previous) standalone spoke or hub. [2] The examples of such screens are the language and network configuration screens in the Anaconda installer that are implemented as standalone spokes because actions available on them have to and should, respectively, take place before all the other actions controlled from the user interface.
The reason for the Hub&Spoke model being used in the Anaconda installer's UI is that it has many advantages:
  • 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 and
  • it can be used for both graphical and textual mode of the installer.
As can be seen in Figure 1, “Hub&spoke model diagram” central points of the Hub&Spoke model are hubs, so let's focus on hubs in the Anaconda installer for a while. Currently there are two of them:
  • the Summary hub that shows a summary of configured options and
  • the Progress hub that shows the progress of the installation process and also shows a summary of options that can be configured during the actual installation being done in the background thread.
Every spoke has a few predefined properties that are reflected on the hub. There are: the ready property stating whether the spoke can be visited or not, the completed property marking the spoke as completed (all required values are set) or not, the mandatory property telling whether the spoke has to be completed before allowing user continue to the next hub or standalone spoke and the status property providing a short summary of the values set in the spoke. To make the layout of the UI clearer, spokes reachable from these two hubs are grouped together to so called categories . A good example of a category used in the Anaconda installer is the Localization category that groups together spokes for keyboard and timezone settings. So to sum it up there are hubs on the hubs there are categories and in the categories there are spokes. Every spoke contains UI controls that show and allow to modify values from one or more subtrees of the in-memory representation of the kickstart file. As we will see in Section 6, “Writing an Anaconda addon” the same applies to addons.

4. Threads and communication

Some of the actions that need to be run during the installation process take a long time to be finished (e.g. scanning disks for existing partitions, downloading package repository metadata, etc). And since basic principle of the GUI programming is that the user interface should be as responsive as possible and since the goal is to allow user configure other settings while some are blocked by the long lasting actions, the Anaconda installer has to run such actions in separate threads. However, the Gtk toolkit doesn't support changing its elements from multiple threads. The Gtk main event loop is running in the main thread of the Anaconda installer process and every code doing some GUI involving actions has to make sure that these actions are run in the main thread as well. The only supported way to do that is by using GLib.idle_add method which is not much comfortable. To facilitate this issue there are some helper functions and decorators defined in the pyanaconda.ui.gui.utils module. The most useful ones are @gtk_action_wait and @gtk_action_nowait decorators that change the decorated function or method in a way that when it is called it is automatically queued to Gtk's main loop, run in the main thread and then the return value is returned to the caller or dropped, respectively.
As it has been already mentioned one of the goals of using multiple threads is to allow user configure something else while some parts of the GUI wait for some actions to finish. The example of such case is package repository metadata download which can take a while and without it being finished the Source and Software spokes cannot be visited as they cannot display valid data. But in the same time there is no reason why e.g. the Datetime spoke should be blocked. Now let's take a look from the blocked spoke's perspective. It waits for some background thread to finish. But once this thread is finished how should the spoke announce it is ready and should not be blocked anymore? The hub that provides an access to the spoke has no relation with the thread the spoke has been waiting for and thus can't tell whether the spoke is yet ready or not. For such cases there is a message queue called hubQ that is being periodically checked in the main event loop. Once the thread the spoke has been waiting for is finished the spoke sends a message [3] that announces the spoke should no longer be blocked. The same applies to the situation when the spoke needs to refresh its status or completion flag. In case of the Progress hub there is the progressQ which serves as a medium to transfer installation progress updates. These mechanisms are needed also for the text mode where the situation is much more complicated, because there is no main loop in the text mode and for vast majority of time it is waiting for an input from keyboard. However there is (a bit experimental) implementation of asynchronous messages handling in TUI and the goal is to make, from spoke's perspective, everything very similar to the graphical user interface.

5. Addon structure

Now that we know bits of how the Anaconda installer looks like and works internally, we can finally focus on the main topic of this guide, i.e. writing an addon for it. Let's take a top-down approach and start with the high-level aspects diving deeper and deeper. The only supported programming language for an Anaconda addon is Python with addon being a Python package. Thus it shouldn't be a surprise that an addon is actually a directory with an __init__.py and other source files and directories (subpackages) in it. And since one package name can be imported in Python only once, the top-level directory of an addon needs to have a unique name. The naming convention suggested for addons is to prefix the addon name with a reversed domain name as (it is common with e.g. D-Bus service names or Java packages) but using dashes instead of dots to separate subdomains so that the directory name is a valid identifier for a Python package. For example org_fedora_hello_world which is used by the Hello world addon from Section 6, “Writing an Anaconda addon”. As it has been mentioned in Section 1, “Introduction” everything should be supported in the kickstart first, then GUI and TUI pieces can come covering some (or all) configuration options from the kickstart part. As these three parts are more or less independent the Anaconda's addon API defines that they should be separate subpackages of the top-level addon package (i.e. subdirectories of the top-level directory) with the following names: ks, gui and tui, with the ks being the only compulsory one. The gui and tui packages should then contain spokes subpackages. [4] All these packages have to contain at least one module with an arbitrary name defining classes inherited from one or more API defined classes. We will get to those classes in Section 6, “Writing an Anaconda addon”.

Important

Do not forget to create all the __init__.py files so that directories really are valid Python packages.

6. Writing an Anaconda addon

We know how the addon's tree-like structure should look like, but obviously the actual work needs to be done in the leafs, the addon's modules. Instead of a lot of words describing how such modules should look like and what they should contain, let's create a simple addon step by step as a practical example. To make it obvious it is just a simple example, we will call it Hello world addon. To get an overall view on the addon and the code it is recommended to clone the Hello world addon's git repository or if it is not possible, at least use the web interface to open the source files. The same applies to the Anaconda's git repository as the installer's sources will be referred many times in the following text.

6.1. Kickstart support

First we need the directories as described in the Section 5, “Addon structure” — the top-level directory giving the addon its name (in this case org_fedora_hello_world) and the directories for separate parts providing code for kickstart, GUI and TUI support. As it was already mentioned many times (intentionally) kickstart support is the most important one so let's start with that part. Its subpackage name is expected to be ks and we will thus need a directory named ks under the addon's top-level directory org_fedora_hello_world. In that directory there has to be the __init__.py file and at least one Python module with arbitrary name. Let's use hello_world.py which conforms to Python's conventions for module names. That brings us to the coding style questions that should be answer before we start with any actual code. The general rule is to follow Python's PEP 8 and PEP 257 (docstring conventions). There is no consensus on the format of the actual content of docstrings in the Anaconda installer. Anything that is well human-readable is okay only if the addon is supposed to have a documentation generated automatically, docstrings should, of course, follow the rules of the toolkit used to generate the documentation. But let's get back to the module with code providing Hello world addon's support for kickstart. The reason why it can have an arbitrary name is that the Anaconda installer looks in all files in the particular directory and collects classes that are inherited from a particular class defined by the API. The same rules apply to all of the ks, gui/spokes and tui/spokes directories containing modules. For the kickstart part of the addon the key class is the AddonData class defined in the pyanaconda.addons module that represents an object for parsing and storing data from the kickstart file. The part of a kickstart file containing data for an addon has the following format:
%addon ADDON_NAME [arguments]
first line
second line
...
%end
Such sequence of lines is called a section. The percent sign followed by the keyword addon marks the beginning of addon section while %end marks its end. In place of the string ADDON_NAME there should be a name of a real addon (like org_fedora_hello_world in our case). Any additional arguments on the addon line will be passed as a list to an instance of the addon's class inherited from the AddonData class. The content between the two lines starting with the percent sign is passed to the instance of the addon's class one line at a time. To make the code as simple as possible, the Hello world addon will just squash the lines passed in a kickstart file to a single line separating the original lines with a space. We know that our addon needs a class inherited from the AddonData with a method handling the %addon argument list and with a method handling lines inside a kickstart %addon section. A quick look into the pyanaconda/addons.py shows these two methods: handle_header takes a list of arguments and the current line numbers (for error reporting), and handle_line takes a single line of content. Let's have a look at the code implementing what we have covered so far.
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()
First few lines of the code describe what could be summed up as the following rule:

Important

Use __all__ variables in modules as it is needed for Anaconda's method for collecting classes to work properly.
Then there is a definition of the HelloWorldData class inherited from the AddonData class with its __init__ method calling the parent's __init__ method and initializing the attributes self.text to an empty string and self.reverse to False. self.reverse is populated in the handle_header method, and self.text is populated in the handle_line method. handle_header uses an instance of the KSOptionParser class provided by pykickstart to parse the additional arguments on the %addon line, and handle_line strips the content lines (removes white space characters at the beginning and the end) and appends them to self.text.
So far our code covers the first phase of the data life cycle in the installation process where data from the kickstart file has to be read. The second phase of the life cycle is updating data with values from the UI which will be covered in the UI code. Then data is used to drive the actual installation process. This is done by two methods with predefined names — setup and execute. The former one is called before the installation transaction starts and should do all changes of the runtime environment an addons needs to do. The later one is called at the end of the transaction and should do all changes to the newly installed (target) system an addon is supposed to do. Again, to make the code as simple as possible, these two methods will be minimalistic. We will need to prepend few imports and a constant definition to the begining of the source file:
import os.path

from pyanaconda.addons import AddonData
from pyanaconda.constants import ROOT_PATH

HELLO_FILE_PATH = "/root/hello_world_addon_output.txt"
And this is how the two methods will look:
    def setup(self, storage, ksdata, instclass):
        """
        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

        """

        # no actions needed in this addon
        pass

    def execute(self, storage, ksdata, instclass, users):
        """
        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)
It should be easy to find out that the setup method does nothing and the execute method just writes the stored text to a file created in the target system's root (/) directory. The most important information delivered by the code above is the number and meaning of the arguments passed to those two methods as described in the docstrings.
That brings us to the last phase of the data life cycle and also the last piece of the code needed in the module providing a kickstart support. At the end of the installation a new kickstart file with the values set in the original kickstart file or during the installation process is written out to the target system's /root directory. It is done by calling the __str__ recursively on the tree-like structure storing the data which means that our class inherited from the AddonData class needs to define its own __str__ method returning its stored data in the format that could be parsed again if the resulting kickstart file was used to install another similar system. It should be obvious how the __str__ method should look like in our case:
    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
By adding this method method we have everything that is needed for a kickstart support done and at the same time we have everything that an addon needs to implement to become a valid addon. Thus we could finish here and start enjoying the warm feeling of writing a new piece of the OS installer. And, believe it or not, we only needed 36 lines of code (not counting the docstrings and comments) to do that. But try to explain how great this is to a majority of people who don't like writing kickstart files and instead prefer clicking on buttons, filling in text entries and so on. To make our code reachable for such people we need to create a user interface for it.

6.2. Graphical user interface

6.2.1. Basic features

Since the textual interface uses a custom toolkit developed for the Anaconda installer, let's start with the Graphical user interface using standard Gtk toolkit that should be more familiar to developers. Again, first we need a subdirectory (subpackage) in the addon's top-level directory (package). The one for the GUI code must be named gui and since there are more types of objects it can provide, it needs to have subdirectories itself. We will start with the most important and most common one — spokes. As it was described in the Section 6.1, “Kickstart support” every part of the addon has to contain at least one module with a definition of a class inherited from a particular class defined by the API. In case of the kickstart support this class was the AddonData class in case of the GUI support there are multiple such classes. But the only recommended one for an addon is the NormalSpoke class defined in the pyanaconda.ui.gui.spokes package. As its name suggests it is a class for the normal spoke screen described in the Section 3, “Hub&Spoke model”. To implement a new class inherited from the NormalSpoke class we need to define the folowing class attributes required by the API:
  • builderObjects that should list 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),
  • mainWidgetName containing the id of the main window widget [5] as defined in the .glade file,
  • uiFile containing the name of the .glade file,
  • category containing the class of the category the spoke belongs to,
  • icon containing the identifier of the icon that will be used for the spoke on the hub and
  • title defining the title that will be used for the spoke on the hub.
The code with all those definitions will then look like this:
# 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")
In the begining two common functions for translations are defined, but with the unusual definition for the _ function. This is caused by the fact that our addon is not meant to have translations. Then we can again see the usage of the __all__ variable to export only the spoke class followed by the first lines of its definition including the definitions of attributes mentioned above. Their values are referencing the widgets defined in the org_fedora_hello_world/gui/spokes/hello.glade file included in the Hello world addon's sources (if you want to open the file, see the begining of the Section 7, “Deploying and testing an Anaconda addon” that lists the packages that are needed). Only two of the attributes deserve a further comment. The first one is the category attribute the value of which is the HelloWorldCategory class imported from the org_fedora_hello_world.gui.categories module. We will get to the HelloWorldCategory definition later, but for now note what was mentioned in the comment just before the import:

Important

The path to addons is in sys.path so things can be imported from the org_fedora_hello_world package.
The second attribute that deserves a comment is the title attribute whose definition contains two underscores. The former one is part of the N_ function name that marks the string for translation, but returns the non-translated version of the string (translation is done later). The latter one is part of the title itself and makes the spoke reachable from the hub with the Alt+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 installer's GUI objects there are two methods initializing a new instance — common Python's __init__ method and the initialize method. The reason for two such functions is that the GUI objects may be created in memory at one time and fully initialized (which can take a longer time) at a different time. Thus the __init__ method should only call the parent's __init__ method and e.g. 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. This is how these two methods look in our case (note the number and description of the arguments passed to the __init__ method):
    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")
Both methods are very simple, but still there are few things deserving a comment. The most important one is the data parametr passed to the __init__ method. It is the in-memory tree-like representation of the kickstart file where all the data is stored. In one of the ancestors' __init__ methods it is stored in the self.data attribute so we can read and modify it in all other methods of the class. Since we have defined the HelloWorldData class in Section 6.1, “Kickstart support” there is a subtree in self.data for our addon and its root (an instance of the HelloWorldData) is available as self.data.addons.org_fedora_hello_world. One of the other things an ancestor's __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.
The __init__ and initialize methods are the two methods that play their roles when the spoke is created. However, the main role of the spoke is to be visited by user who wants to change or review some values it shows and sets. There are three methods — refresh, apply and execute — that handle things that need to be done when the spoke is entered and left. The refresh method is called when the spoke is about to be visited by the user and its responsibility is to refresh the spoke's state (mainly it's UI elements) to reflect the current values stored in the self.data structure. The apply and execute methods are called when the spoke is left and they should store values set in the UI elements to the self.data structure and do all runtime changes the spoke requires based on its current state, respectively.
The implementations of those three functions are very simple in the Hello world addon:
    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
So far we have covered methods that can be used to instantiate and visit spoke. It may seem like everything that is needed, but not every spoke can be visited anytime (e.g. What would be the point of software selection being shown before the repository is set?) and while values shown and controlled by some spokes are crucial to the installation process and cannot be omitted, some spokes allow modification of optional values with minor effect on the installed system. That's why all spokes have the ready, completed and mandatory properties. As their names suggest, these properties determine if the spoke is ready to be visited, if the spoke is completed (i.e. all values it requires to be set are set) and if the spoke is mandatory to be completed for the installation to continue. All these attributes of the spoke need to be dynamically determined based on the current state of the installer/installation process. Here comes the trivial implementation of those properties from the Hello world addon which requires some value to be set in the HelloWorldData's text attribute:
    @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
With those three properties defined, a spoke can tell users whether they may, have to or cannot visit the spoke. Nevertheless, users seeing the hub need to decide whether to visit the spoke or not. That's why every spoke also has the status property, which is supposed to provide a short (one-line) summary describing values set on the spoke. Since the only value managed by the spoke is the text it shows, allows to edit and stores in the self.data structure, it is only logical to use that text as the status. Also, the status should warn user if no text is set:
    @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")
And that's it! Less than 60 lines of code (the real code lines) are needed to implement the basic GUI of an addon. Of course, it is a trivial addon that does nothing useful, but anything else is just a common Python Gtk programming with some minor specific restrictions. For example, as was mentioned in the begining of this section, every spoke has to have its main window — an instance of the SpokeWindow widget. This widget (together with some more Anaconda-specific widgets) exists in the anaconda-widgets package and files needed for development (e.g. Glade definitions) live in the anaconda-widgets-devel package.

6.2.2. Advanced features

Because many spokes have a lot in common, the pyanaconda package contains a lot of helper and utility functions and constructs, that may be used by hubs and spokes. Majority of them are available in the pyanaconda.ui.gui.utils module. The Hello world addon demonstrates usage of the utility, that is used in a number of Anaconda's GUI code — the englightbox context manager, that puts a window into a lightbox to make it better visible, focused and to prevent users from interaction with the underlying window. To demonstrate such functionality, the Hello world addon's GUI contains a button that runs a dialog. The dialog itself is a special class HelloWorldDialog inheriting from the GUIObject class defined in the pyanaconda.ui.gui.__init__ module. The dialog class defines the run method that runs and destroys an internal Gtk dialog accessible through the self.window attribute that is populated thanks to the mainWidgetName class attribute with the same meaning as in case of the spoke. The code running the dialog within a lightbox is then very simple:
        # every GUIObject gets ksdata in __init__
        dialog = HelloWorldDialog(self.data)

        # show dialog above the lightbox
        with enlightbox(self.window, dialog.window):
            dialog.run()
The code simply creates an instance of the dialog and then uses the 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.
Another useful feature of the Anaconda's codebase is a possibility to define a spoke that will appear in the installation process as well as in the first boot when the Initial Setup tool reusing the pyanaconda package runs. All that is needed to make spoke available in the Initial Setup is to inherit the special FirstbootSpokeMixIn class (or more precisely mixin) as the first inherited class. The other option is the FirstbootOnlySpokeMixIn [6] class/mixin which, as its name suggests, make spoke appear only in the Initial Setup utility.
There are many more advanced features provided by the pyanaconda package (like the @gtk_action_wait and @gtk_action_nowait decorators), but they are out of scope of this guide. Readers are recommended to go through the Anaconda installer's sources that contain a lot of examples.

6.3. Textual user interface

The Section 1, “Introduction” mentions that apart from the GUI installation, the Anaconda installer also supports text mode installation that may be the only choice on some hardware configurations. The previous Section 6.2, “Graphical user interface” describes, how it is possible for an addon to define and implement graphical screens for the installer allowing user interaction. Now it's time to have a look at the text mode which is based on the Anaconda's simpleline toolkit that is suitable for purely textual output without any "advanced" features like colours, fonts, cursor movement etc.
Internally, there are three main classes in the simpleline toolkit — App, UIScreen and Widget. Widgets, which are elemental units containing the information to be shown (printed) to the user, are placed on the UIScreens that are switched by a single instance of the App class. On top of those basic elements there are hubs, spokes and dialogs all containing various widgets similarly as in the GUI. So from the addon's perspective, the most important classes are the NormalTUISpoke and various other classes defined in the pyanaconda.ui.tui.spokes package. All those classes are based on the TUIObject class which is an equivalent of the GUIObject class mentioned in the previous chapter.
Creating a text spoke for an addon again means creating subpackages of the main addon's package. This time it has to be named tui and the directory for spokes has to be, surprisingly, named spokes. A TUI spoke is again a Python class this time inheriting from the NormalTUISpoke class overriding special arguments and methods defined by the API. Because the TUI is simpler than the GUI there are fewer such arguments, namely two — title and category. The former one has the same meaning and type as in the GUI case, but the latter one has to be a string in text mode. Categories are handled in a different way in the GUI and TUI code of the Anaconda installer. [7] However, categories are used only for grouping and ordering spokes on hubs in the text mode (their titles are not shown anywere), so the easiest thing is to use some preexisting category (a new TUI category would require a patch for the Anaconda installer).
Apart from the two arguments, the spoke class is expected to define (override) a few methods — __init__, initialize, refresh, refresh, apply, execute, input, prompt — and properties described in the Section 6.2, “Graphical user interface” for the case of a GUI spoke. Let's have a look on a trivial TUI spoke defined in the Hello world addon. We could start with the methods:
    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: ")
There is no need to override the __init__ method if it just calls the ancestor's __init__ method, but the comments in the example code describe the arguments passed to constructors of spoke classes in an understandable way. The initialize method just sets up a default value for the spoke's internal attribute which is then updated by the refresh method and used by the apply method to update the kickstart data. The only differences from the GUI equivalents of those two methods are the return type of the refresh method that is bool instead of None and an additional args argument it takes. The meaning of the returned value is explained in the comment — it tells the application (the single App class instance) whether that spoke requires input from user or not. The additional args argument is used for passing extra information to the spoke when it scheduled. Then there is also the execute method with the same purpose as in the GUI and with the same pass statement doing all that is needed in such a trivial case.
The input and prompt methods are TUI-specific and as thier names suggest they are responsible for interaction with the user. The prompt method should simply return the prompt that should be printed once the content of the spoke is printed. After a user enters some string in a reaction to the prompt, the entered string is passed to the input method for processing. The input method usually needs to parse the input and take some actions based on its type and value. Since our spoke just asks for some value, the value is simply stored in an internal attribute. But typically there are some non-trivial actions done like accepting the 'c' or 'r' inputs for continuing or refreshing, respectively, converting numbers into integers and showing additional screens or toggling bool values based on them etc. In contrast to the GUI code, the apply method is not called automatically when leaving the spoke, so we need to call it explicitly from the input method. The same applies to closing (hiding) the spoke's screen done by calling the close method of the spoke. If we want to show another screen (need some additional info from user entered in a different spoke, dialog, etc.) we can also instantiate another TUIObject here and call one of the self.app.switch_screen* methods of the App. The last interesting thing about the input method is its return value. It has to be either INPUT_PROCESSED or INPUT_DISCARDED constant (both defined in the pyanaconda.constants_text module) or the input string itself in case such an input should be processed by some other screen.
Since the restrictions of the text user interface are quite strong the TUI spokes typically have a very similar structure — a list of checkboxes or entries that should be (un)checked or populated by the user. The previous paragraphs show the imperative way of implementing such TUI spoke where the spoke's methods handle printing and processing of the available and provided data. However, there is a different, simpler way of doing that by using the declarative EditTUISpoke class from the pyanaconda.ui.tui.spokes package. By inheriting from this class, it is possible to implement a typical TUI spoke by just specifying fields and attributes that should be set on the spoke. The following code defines an example spoke implemented that way:
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
The auxiliary class _EditData just serves as a data container that is used for storing values entered by the user. The HelloWorldEditSpoke 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 and the second one that is shown only if there is some non-empty value set in the first one. The comments in the example code should be enough explanatory to guide reader through the declarative definition of a TUI spoke by using the EditTUISpoke class.

7. Deploying and testing an Anaconda addon

As was mentioned in the previous section, there are some packages required for development of an Anaconda addon. In particular the anaconda-widgets and anaconda-widgets-devel packages that contain the widgets, glade specifications etc. and the anaconda package that contains the pyanaconda Python package (needed for running pylint checks and so on).
To test a newly created addon one needs to put it in the installation environment and let the Anaconda installer collect addon's classes and definitions. Addons are collected from the /usr/share/anaconda/addons/ directory, that is expected to contain addons' Python packages (directory trees). The easiest way to achieve that is to create the usr/share/anaconda/addons/ directory tree somewhere, place a copy of the addon's package into it and then run the following command from the top of the usr/share/anaconda/addons/ directory tree (i.e. from the directory containing the usr directory):
$ find . |cpio -c -o |gzip -9 > addon_updates.img
The result is a gzipped cpio archive, but at the same time a so-called updates image that can be uploaded to a web server or copied to a USB drive and used to update the installation environment. If a boot option formatted as "updates=UPDATES_IMAGE_URL" is used when booting into the installer, Anaconda fetches the updates image, and unpacks it to the installation environment. [8] If everything goes well, all addon's classes are collected by the installer, the kickstart section (if any) is passed to the addon to process, spokes are shown on the hub etc.
Once an addon is tested and ready to be deployed it should be packaged as a Fedora package to facilitate creation of the installation images containing the addon (so-called composes . That requires writing a so-called spec file which defines and describes an RPM package. The basic structure of an addon spec file may look like this:
Name:           example-anaconda-addon
Version:        0.1
Release:        1%{?dist}
Summary:        Anaconda addon useful for something in the installation process

License:        GPLv2+
URL:            https://git.fedorahosted.org/cgit/example-anaconda-addon.git

Source0:        %{name}-%{version}.tar.gz

BuildArch:      noarch
BuildRequires:	python2-devel
BuildRequires:  anaconda >= 19
Requires:       anaconda >= 19

%description
This is an addon that brings some useful additional functionality to the
Anaconda installer.

%prep
%setup -q

%build

%check
make test

%install
make install DESTDIR=%{buildroot}

%files
%{_datadir}/anaconda/addons/org_fedora_example

%doc COPYING ChangeLog README

%changelog
* Mon Jan 6 2014 Great Author <great.author@example.com> - 0.1-1
- Initial RPM for the example-anaconda-addon
Such spec file makes use of the make utility with the following example Makefile:
NAME = example-anaconda-addon

VERSION = 0.1

ADDON = org_fedora_example
TESTS = tests

FILES = $(ADDON) \
	$(TESTS) \
	COPYING \
	Makefile \
	README

EXCLUDES = \
	*.pyc

all:
	@echo "usage: make dist"
	@echo "       make test"
	@echo "       make install"
	@echo "       make uninstall"

DISTNAME = $(NAME)-$(VERSION)
ADDONDIR = /usr/share/anaconda/addons/
DISTBALL = $(DISTNAME).tar.gz

install:
	mkdir -p $(DESTDIR)$(ADDONDIR)
	cp -rv $(ADDON) $(DESTDIR)$(ADDONDIR)

uninstall:
	rm -rfv $(DESTDIR)$(ADDONDIR)

dist:
	rm -rf $(DISTNAME)
	mkdir -p $(DISTNAME)
	@if test -d ".git"; \
	then \
		echo Creating ChangeLog && \
		( cd "$(top_srcdir)" && \
		  echo '# Generate automatically. Do not edit.'; echo; \
		  git log --stat --date=short ) > ChangeLog.tmp \
		&& mv -f ChangeLog.tmp $(DISTNAME)/ChangeLog \
		|| ( rm -f ChangeLog.tmp ; \
		     echo Failed to generate ChangeLog >&2 ); \
	else \
		echo A git clone is required to generate a ChangeLog >&2; \
	fi
	for file in $(FILES); do \
		cp -rpv $$file $(DISTNAME)/$$file; \
	done
	for excl in $(EXCLUDES); do \
		find $(DISTNAME) -name "$$excl" -delete; \
	done
	tar -czvf $(DISTBALL) $(DISTNAME)
	rm -rf $(DISTNAME)

test:
	PYTHONPATH=. nosetests --processes=-1 -vw tests/

8. Addons welcomed

As has been described in the Section 1, “Introduction”, there are many things that could be added to the Anaconda installer to facilitate installation and configuration of the operating system. This guide is supposed to be the first place to start the adventure of Anaconda addon development and hopefully it shows it's quite easy and peaceful to write a brand new piece of an OS installer. Apart from the trivial Hello world addon there exists the OSCAP Anaconda Addon project that may be used as a source of additional information, tips and patterns useful for addon development. Hopefully there will soon be more addons created by various developers coming from various projects.
This guide and the examples of addons are definitely not covering 100 % of the trick&treat game of Anaconda addon development. However, members of the Anaconda installer team (and in particular Vratislav Podzimek), are always willing to help addon developers with their questions and issues. The best place to ask is the anaconda-devel mailing list that is read and moderated by the Anaconda developers.
If there is a nice feature that should be added to the Anaconda installer's codebase and API or if there is a bug, please send tested patches and pylint-checked [9] patches to the anaconda-patches mailing list for a review. If accepted, one of the developers will push the patches to the Anaconda's git repository.
Keep in mind, addons and patches welcomed!

A. Revision History

Revision History
Revision 1-0Wed Jan 15 2014Vratislav Podzimek
First version to be a part of the official documentation
Revision 0-0Fri Dec 28 2012Vratislav Podzimek
Initial creation of book by publican

Index

A

ABRT , Architecture
addon, Introduction
Anaconda, Introduction
Anaconda git , Writing an Anaconda addon
anaconda-devel-list, Addons welcomed
anaconda-patches-list , Addons welcomed

B

blivet, Architecture

D

Datetime spoke , Threads and communication

E

execute , Architecture

F

Firstboot, Introduction

I

Initial Setup, Introduction

K

kickstart, Introduction

L

libreport , Architecture
Localization category , Hub&Spoke model

O

OSCAP Anaconda Addon, Addons welcomed

P

Progress hub , Hub&Spoke model
progressQ, Threads and communication
pyanaconda, Architecture
pykickstart, Architecture
python-meh , Architecture

S

setup , Architecture
Software spoke , Threads and communication
Source spoke , Threads and communication
spec , Deploying and testing an Anaconda addon
spokes' properties , Hub&Spoke model
Summary hub , Hub&Spoke model


[1] At the same time, it was obvious that the old Firstboot had to be kept working because of the third-party modules written for it.
[2] Screens mentioned in the rest of this section are screens from the graphical mode of the installation.
[3] internally it means that it puts a message to the queue
[4] The gui package may also contain a categories subpackage if the addon needs to define a new category, but this is not recommended.
[5] an instance of the SpokeWindow widget which is a custom widget created for the Anaconda installer
[6] both those classes are defined in the pyanaconda.ui.common module
[7] which is likely to change in the future to sticking to the better (GUI) way
[8] overwriting the files that already exist with the files from the updates image (which is how the Anaconda developers use those files for testing patches)
[9] to facilitate testing and pylint checks, the make check command may be used to run tests and checks with quite effective false-positives handling