Product SiteDocumentation Site

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 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()
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.iutil import getSysroot

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

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(getSysroot() + 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\n" % 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.