Product SiteDocumentation Site

6. Writing an Anaconda add-on

The sections below will demonstrate the process writing and testing a sample add-on called Hello World. This sample add-on will support all interfaces (Kickstart, GUI and TUI). Sources for this sample add-on are available on GitHub in the rhinstaller/hello-world-anaconda-addon repository; it is recommended to clone this repository or at least open the sources in the web interface.
Another repository to review is rhinstaller/anaconda, which contains the installer source code; it will be referred to in several parts of this section as well.
Before you begin developing the add-on itself, start by creating its directory structure as described in Section 5, “Anaconda Add-on Structure”. Then, continue with Section 6.1, “Kickstart Support”, as Kickstart support is mandatory for all add-ons. After that, you can optionally continue with Section 6.2, “Graphical user interface” and Section 6.3, “Text User Interface” if needed.

6.1. Kickstart Support

Kickstart support is always the first part of any add-on that should be developed. Other packages - support for the graphical and text-based interface - will depend on it. To begin, navigate to the org_fedora_hello_world/ks/ directory you have created previously, make sure it contains an __init__.py file, and add another Python script named hello_world.py.
Unlike built-in Kickstart commands, add-ons are used in their own sections. Each use of an add-on in a Kickstart file begins with an %addon statement and is closed by %end. The %addon line also contains the name of the add-on (such as %addon org_fedora_hello_world) and optionally a list of arguments, if the add-on supports them.
An example use of an add-on in a Kickstart file looks like the example below:

Example 2. Using an Add-on in a Kickstart File

%addon ADDON_NAME [arguments]
first line
second line
...
%end
The key class for Kickstart support in add-ons is called AddonData. This class is defined in pyanaconda.addons and represents an object for parsing and storing data from a Kickstart file.
Arguments are passed as a list to an instance of the add-on class inherited from the AddonData class. Anything between the first and last line is passed to the add-on's class one line at a time. To keep the example Hello World add-on simple, it will merge all lines in this block into a single line and separate the original lines with a space.
The example add-on requires a class inherited from AddonData with a method for handling the list of arguments from the %addon line, and a method for handling lines inside the section. The pyanaconda/addons.py module contains two methods which can be used for this:
  • handle_header - takes a list of arguments from the %addon line (and line numbers for error reporting)
  • handle_line - takes a single line of content from between the %addon and %end statements
The example below demonstrates a Hello World add-on which uses the methods described above:

Example 3. Using handle_header and handle_line

from pyanaconda.addons import AddonData
from pykickstart.options import KSOptionParser

# export HelloWorldData class to prevent Anaconda's collect method from taking
# AddonData class instead of the HelloWorldData class
# :see: pyanaconda.kickstart.AnacondaKSHandler.__init__
__all__ = ["HelloWorldData"]

HELLO_FILE_PATH = "/root/hello_world_addon_output.txt"

class HelloWorldData(AddonData):
    """
    Class parsing and storing data for the Hello world addon.

    :see: pyanaconda.addons.AddonData

    """

    def __init__(self, name):
        """
        :param name: name of the addon
        :type name: str

        """

        AddonData.__init__(self, name)
        self.text = ""
        self.reverse = False

    def handle_header(self, lineno, args):
        """
        The handle_header method is called to parse additional arguments in the
        %addon section line.

        :param lineno: the current linenumber in the kickstart file
        :type lineno: int
        :param args: any additional arguments after %addon <name>
        :type args: list
        """

        op = KSOptionParser()
        op.add_option("--reverse", action="store_true", default=False,
                dest="reverse", help="Reverse the display of the addon text")
        (opts, extra) = op.parse_args(args=args, lineno=lineno)

        # Reject any additional arguments.
        if extra:
            msg = "Unhandled arguments on %%addon line for %s" % self.name
            if lineno != None:
                raise KickstartParseError(formatErrorMsg(lineno, msg=msg))
            else:
                raise KickstartParseError(msg)

        # 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()
The example begins by importing necessary methods and defining an __all__ variable which is necessary to prevent Anaconda's collect method from taking the AddonData class instead of add-on specific HelloWorldData.
Then, the example shows a definition of the HelloWorldData class inherited from AddonData with its __init__ method calling the parent's __init__ and initializing the attributes self.text and self.reverse to False.
The self.reverse attribute is populated in the handle_header method, and the self.text is populated in handle_line. The handle_header method uses an instance of the KSOptionParser provided by pykickstart to parse additional options used on the %addon line, and handle_line strips the content lines of white space at the beginning and end of each line, and appends them to self.text.
The code above covers the first phase of the data life cycle in the installation process: it reads data from the Kickstart file. The next step is to use this data to drive the installation process. Two predefined methods are available for this purpose:
  • setup - called before the installation transaction starts and used to make changes to the installation runtime environment
  • execute - called at the end of the transaction and used to make changes to the target system
To use these two methods, you must add some new imports and a constant to your module, as shown in the following example:

Example 4. Importing the setup and execute Methods

import os.path

from pyanaconda.addons import AddonData
from pyanaconda.constants import getSysRoot

from pykickstart.options import KSOptionParser
from pykickstart.errors import KickstartParseError, formatErrorMsg

HELLO_FILE_PATH = "/root/hello_world_addon_output.txt"
An updated example of the Hello World add-ons with the setup and execute methods included is below:

Example 5. Using the setup and execute Methods

    def setup(self, storage, ksdata, instclass, payload):
        """
        The setup method that should make changes to the runtime environment
        according to the data stored in this object.

        :param storage: object storing storage-related information
                        (disks, partitioning, bootloader, etc.)
        :type storage: blivet.Blivet instance
        :param ksdata: data parsed from the kickstart file and set in the
                       installation process
        :type ksdata: pykickstart.base.BaseHandler instance
        :param instclass: distribution-specific information
        :type instclass: pyanaconda.installclass.BaseInstallClass
        :param payload: object managing packages and environment groups
                        for the installation
        :type payload: pyanaconda.packaging.dnfpayload.DNFPayload

        """

        # no actions needed in this addon
        pass

    def execute(self, storage, ksdata, instclass, users, payload):
        """
        The execute method that should make changes to the installed system. It
        is called only once in the post-install setup phase.

        :see: setup
        :param users: information about created users
        :type users: pyanaconda.users.Users instance

        """

        hello_file_path = os.path.normpath(getSysroot() + HELLO_FILE_PATH)
        with open(hello_file_path, "w") as fobj:
            fobj.write("%s\n" % self.text)
In the above example, the setup method does nothing; the Hello World add-on does not make any changes to the installation runtime environment. The execute method writes stored text into a file created in the target system's root (/) directory.
The most important information in the above example is the amount and meaning of the arguments passed to the two new methods; these are described in docstrings within the example.
The final phase of the data life cycle, as well as the last part of the code needed in a module providing Kickstart support, is generating a new Kickstart file, which includes values set at installation time, at the end of the installation process as described in Section 2, “Architecture of Anaconda”. This is performed by calling the __str__ method recursively on the tree-like structure storing installation data, which means that the class inherited from AddonData must define its own __str__ method which returns its stored data in valid Kickstart syntax. This returned data must be possible to parse again using pykickstart.
In the Hello World example, the __str__ method will be similar to the following example:

Example 6. Defining a __str__ Method

    def __str__(self):
        """
        What should end up in the resulting kickstart file, i.e. the %addon
        section containing string representation of the stored data.

        """

        addon_str = "%%addon %s" % self.name

        if self.reverse:
            addon_str += " --reverse"

        addon_str += "\n%s\n%%end\n" % self.text
        return addon_str
Once your Kickstart support module contains all necessary methods (handle_header, handle_line, setup, execute and __str__), it becomes a valid Anaconda add-on. You can continue with the following sections to add support for the graphical and text-based user interfaces, or you can continue with Section 7, “Deploying and Testing an Anaconda Add-on” and test the add-on.