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 ... %endSuch 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()
Important
__all__
variables in modules as it is needed for Anaconda's method for collecting classes to work properly.
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.
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"
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)
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.
/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