From Fedora Project Wiki

(Moved the guide to the pykickstart repo)
Tag: Replaced
 
Line 1: Line 1:
 +
{{autolang|base=yes}}
  
 
+
The pykickstart programmer's guide has moved to the [https://github.com/pykickstart/pykickstart/blob/master/docs/programmers-guide pykickstart GitHub repository]
== pykickstart Programmer's Guide ==
 
''by Chris Lumens''
 
(written April 13, 2007)
 
 
 
 
 
=== Introduction ===
 
pykickstart is a Python library for manipulating kickstart files.  It
 
contains a common data representation, a parser, and a writer.  This
 
library aims to be useful for all Python programs that need to work with
 
kickstart files.  The two most obvious examples are anaconda and
 
system-config-kickstart.  It is recommended that all other tools that need
 
to use kickstart files use this library so that we can maintain equivalent
 
levels of support across all tools.
 
 
 
The kickstart file format itself has only been defined in a rather ad-hoc
 
manner.  Various documents describe the format, commands, and their
 
effects.  However, each kickstart-related program implemented its own
 
parser.  As anaconda added support for new commands and options, other
 
programs drifted farther and farther out of sync with the "official"
 
format.  This leads to the problem that valid kickstart files are not
 
accepted by all programs, or that programs will strip out options it
 
doesn't understand so that the input and output files do not match.
 
 
 
pykickstart is an effort to correct this.  It was originally designed to
 
be a common code base for anaconda and system-config-kickstart, so making
 
the code generic and easily extensible were top priorities.  Another
 
priority was to formalize the currently recognized grammar in an easily
 
understood parser so that files that used to work would continue to.  I
 
believe these goals have been met.
 
 
 
pykickstart also understands all the various versions of the kickstart syntax
 
that have been around.  Various releases of Red Hat Linux, Red Hat Enterprise
 
Linux, and Fedora Core have had slightly different versions.  For the most
 
part, the basic syntax has stayed the same.  However, different commands have
 
come and gone and different options have been supported on those commands.
 
pykickstart allows specifying which version of kickstart syntax you want
 
to support for reading and writing, allowing you to use one code base to
 
deal with the full range of kickstart files.
 
 
 
This document will cover how to use pykickstart in your programs and how to
 
extend the basic parser to get customized behavior.  It includes a
 
description of the important classes and several working examples.
 
 
 
 
 
=== Getting Started ===
 
Before diving into the full documentation, it is useful to see an example
 
of how simple it is to use the default pykickstart in your programs.  Here
 
is a code snippet that imports the required classes, parses a kickstart
 
file, and leaves the results in the common data format:
 
<code>
 
        #!/usr/bin/python
 
        from pykickstart.parser import *
 
        from pykickstart.version import makeVersion
 
        ksparser = KickstartParser(makeVersion())
 
        ksparser.readKickstart("ks.cfg")
 
</code>
 
 
 
The call to makeVersion() creates a new kickstart handler object for the
 
specified version.  By default, it creates one for the latest supported
 
syntax version.  The call to KickstartParser() creates a new parser using
 
the handler object for dealing with individual commands.  The call to
 
readKickstart() then reads in the kickstart file and sets values in the
 
handler.
 
 
 
After this call, all the data from the input kickstart file has been set
 
on the command objects.  You can see which objects are available by
 
running dir(ksparser.handler), and then inspect various data settings by
 
examining the contents of each of those objects.
 
 
 
The data can be modified if you want.  You can then write out the contents
 
to a new file by simply calling:
 
 
 
<code>
 
        outfile = open("out.cfg", 'w")
 
        outfile.write(kshandlers.__str__())
 
        outfile.close()
 
</code>
 
 
 
=== Files ===
 
The important classes that make up pykickstart are spread across a handful
 
of files.  This section includes a brief outline of the contents of those
 
classes.  For more thorough documentation, refer to the python doc strings
 
throughout pykickstart.  In python, you can view these docs strings like
 
so:
 
<code>
 
        >>> from pykickstart import parser
 
        >>> help(parser)
 
        >>> help(parser.KickstartParser)
 
</code>
 
 
 
==== base.py ====
 
This file contains several basic classes that are used throughout the rest
 
of the library.  For the most part, these are abstract classes that are
 
not important to most users of pykickstart.  You will really only need to
 
deal with these classes if you are extending kickstart syntax.  Other
 
users will mainly only want to look at these classes to see what methods
 
and attributes are provided.  This information is also available from the
 
docs strings.
 
 
 
BaseData, BaseHandler, and KickstartCommand are abstract classes that
 
define common methods and attributes.  These classes may not be used
 
directly - they can only be used if subclassed.  The BaseData and
 
KickstartCommand classes are subclassed to create data objects and command
 
objects.  BaseHandler is subclassed to create version handlers that drive
 
the processing of commands and the setting of data.
 
 
 
DeprecatedCommand is a subclass of KickstartCommand that may be further
 
used as a subclass for command objects.  When one of these subclasses is
 
used, a warning message is printed.  Commands that are deprecated are
 
recognized by the parser, but any options given will not be processed and
 
their use causes a warning message to be printed.
 
 
 
==== constants.py ====
 
This file includes no classes, though it does include several important
 
constants representing various things in a kickstart handler class.  You
 
should import its contents like so:
 
<code>
 
        from pykickstart.constants import *
 
</code>
 
 
 
==== error.py ====
 
This file contains several useful exceptions and methods.  There are four
 
basic exceptions in pykickstart:  KickstartError, KickstartParseError,
 
KickstartValueError, and KickstartVersionError.
 
 
 
KickstartError is a generic exception, raised on conditions that are not
 
appropriate for any of the other more specific exceptions.
 
 
 
If the parser encounters an error while reading your input file, it will
 
raise a KickstartParseError with the line in question.  Examples of errors
 
include bad options given to section headers, include files not existing,
 
or headers given for sections that are not recognized (for instance,
 
typos).  If the parser encounters an error while processing the arguments
 
to a command, it will raise a KickstartValueError.  Examples of these
 
sorts of errors include too many or too few arguments, or missing required
 
arguments.
 
 
 
KickstartVersionError is only raised by the methods in pykickstart.version
 
if an invalid version is provided by the user.
 
 
 
Error messages should call formatErrorMsg() to be properly formatted
 
before being sent into an exception.  A properly formatted error message
 
includes the line number in the kickstart file where the problem occurred
 
and optionally, a more descriptive message.
 
 
 
==== option.py ====
 
This file contains the KSOptionParser and KSOption classes, which are
 
specialized subclasses of OptionParser and Option from python's optparse
 
module.  These classes are used extensively throughout the parser and
 
command objects.  Specialized subclasses are needed to support required,
 
deprecated, and versioned options; handle specialized error reporting; and
 
support additional option types.
 
 
 
==== parser.py ====
 
This file represents the bulk of pykickstart code.  At its core is the
 
KickstartParser class, which is essentially a state machine.  There is one
 
state for each of the sections in a kickstart file, plus some specialized
 
ones to make the parser work.  The readKickstart() method is the entry point
 
into this class and is designed to be as generic as possible.  It reads
 
from the given file name.  It is also possible that you may want to read
 
from an existing string, so readKickstartFromString() is also provided.
 
 
 
With the exception of _stateMachine(), all the methods in KickstartParser
 
may be overridden in a subclass.  _stateMachine() should never be
 
overridden, however, as it provides the core logic for processing
 
kickstart files.
 
 
 
There are a few other minor points to note about KickstartParser.  When
 
creating a KickstartParser object, you can set the followIncludes
 
attribute to False if you do not wish for include files to be looked up
 
and parsed as well.  There are several instances when this is handy.  You
 
can also set the missingIncludesIsFatal attribute to False if you want to
 
ignore missing include files.  This is most useful when you only care
 
about the main kickstart file (like in ksvalidator, for instance).  Note
 
that you can pass None in for kshandlers in the special case if you do not
 
care about handling any commands at all.  As we will see later, this is
 
useful in one special case.
 
 
 
The Script class represents a single script found in the kickstart file.
 
Since there can be several scripts in a single file, all the instances of
 
Script are stored in a single list.  Somewhat confusingly, this list is
 
stored in the handler object provided to KickstartParser when it is
 
instantiated.  The script list is not stored in the parser itself.  There
 
are three different types of scripts - pre, post, and traceback.  The
 
script class contains an attribute that may be used to discriminate among
 
types.
 
 
 
Finally, the parser.py file contains a Packages class for representing the
 
%packages section of the kickstart file.  It includes three separate lists
 
- a list of packages to install, a list of packages to exclude, and a list
 
of groups to install.  It does not contain anything to handle the header
 
of the %packages section, as this is done by the parser.  The Packages
 
instance is held in the same place as the script list.
 
 
 
==== version.py ====
 
pykickstart supports processing multiple versions of the kickstart syntax
 
from the same code base.  In order to make use of this functionality,
 
users must request objects by version number.  This file provides the
 
methods and attributes to make this easy.  Versions are specified by
 
symbolic names that match up with the names of Fedora or Red Hat
 
Enterprise Linux releases.
 
 
 
There is also a special DEVEL version that maps to the latest supported
 
syntax version.  All methods in version.py take DEVEL as the implied
 
version, so most people should never even need to deal with specifying
 
their own version.
 
 
 
stringToVersion() and versionToString() map between strings and these
 
symbolic names.  These are provided to make using pykickstart a little
 
easier.  stringToVersion() allows you to take the contents of
 
/etc/redhat-release and get a pykickstart version right from that.
 
 
 
returnClassForVersion() returns the class that matches a specific version.
 
Most people will not need this capability, as what they are really after
 
is an instance of that class.  makeVersion() returns that instance.
 
 
 
 
 
=== Handler Classes ===
 
Kickstart syntax versions are each represented by a file in the handlers/
 
subdirectory.  For the most part, these are extremely simple files that
 
define a subclass of the BaseHandler class mentioned above.  The names of
 
the handler files are important, but this only matters when adding support
 
for new syntax version.  This will be discussed in a later section.
 
 
 
The control.py file is a little more complicated, however.
 
 
 
==== control.py ====
 
This file contains two dictionaries.  The commandMap defines a mapping
 
from a syntax version number (as returned by version.stringToVersion()) to
 
another dictionary.  This dictionary defines a mapping from a command
 
string to an object that processes that command.  Multiple strings may map
 
to the same object, since some kickstart commands have multiple names
 
("part" and "partition", for instance).
 
 
 
The dataMap is set up similarly.  It maps syntax version numbers to
 
further dictionaries.  These dictionaries map data object names to the
 
objects themselves.  Unlike the commandMap, each name may only map to a
 
single object.  However, multiple instances of each object can exist at
 
once as these instances are stored in lists.  This entire setup is
 
required to handle the data for commands such as "network" and "logvol",
 
which can be specified several times in one kickstart file.
 
 
 
The structures in control.py look to be much more verbose than required.
 
Since much data is duplicated among all the various substructures, it
 
seems like this is a perfect place for better object oriented design.
 
However, the duplication is considered a benefit in this one case.  It can
 
be difficult to tell which commands are supported by each syntax version,
 
and what object handles those commands.  The verbosity in this file makes
 
it very clear exactly which objects will be used by each version of
 
kickstart.
 
 
 
 
 
=== Command Classes ===
 
In the commands/ subdirectory you will see many files.  Each file
 
corresponds to a single kickstart command.  At a minimum, one file will
 
contain a single class that implements the parser, writer, and data store
 
for that command.  This command is then entered into the appropriate place
 
in the commandMap from control.py, and then called in the right places by
 
the parser.
 
 
 
These files may be slightly more complicated, however.  Some files contain
 
several classes that all do the same thing.  This is because there have
 
been multiple versions of the syntax for that command, and there is one
 
class per version.  They are all grouped in the same file for ease of
 
readability, and later versions are allowed to inherit from earlier
 
versions by means of subclassing.
 
 
 
Each file may also contain one or more data objects.  These data objects
 
are the same as the contents of the dataMap from control.py.  There may
 
also be several versions of each data object.
 
 
 
At a minimum, the command classes and data classes must implement the
 
methods from KickstartCommand and BaseData.  In particular, __init__,
 
__str__, and parse will be called by the KickstartParser.  An exception
 
will be raised if one is not defined and the abstract class's method is
 
called instead.
 
 
 
There are a couple important things to know about command classes.  The
 
more complex commands have a lot of data attributes.  In writing code to
 
deal with these commands, it can be very tedious to write things like:
 
<code>
 
  ksparser.handler.bootloader.forceLBA = True
 
  ksparser.handler.bootloader.linear = False
 
  ksparser.handler.bootloader.password = "blah"
 
</code>
 
As a shortcut, command classes provide a __call__ method that allows a
 
much more concise and natural way to set a lot of attributes.  Any keyword
 
arguments to a command's __init__ method may be passed like this:
 
<code>
 
  ksparser.handler.bootloader(forceLBA=True, linear=False, password="blah")
 
</code>
 
Also, all command classes have a writePriority attribute.  This controls
 
the order in which commands will be written out when
 
KickstartParser.__str__ is called.  This is needed because the order of
 
certain commands matters to anaconda.  Lower numbered commands will be
 
written out before higher numbered ones.  If several classes have the same
 
priority, they are written in alphabetical order.
 
 
 
 
 
=== Extending pykickstart ===
 
By default, pykickstart reads in a kickstart file and sets values in the
 
command objects.  This is useful for some applications, but not all.
 
anaconda in particular has some odd requirements so it will be our basis
 
for examples on extending pykickstart.
 
 
 
==== Only paying attention to one command ====
 
Sometimes, you only want to take action on a single kickstart command and
 
don't care about any of the others.  anaconda, for instance, supports a
 
vnc command that needs to be processed well before any of the other
 
commands.  pykickstart has some functionality to handle this.  Version
 
handlers maintain an internal dictionary mapping commands to objects.  By
 
setting objects in this dictionary to None, pykickstart knows to ignore
 
them.
 
 
 
Luckily, you don't have to deal with these internal data structures
 
yourself.  All that is required is to create a special BaseHandler
 
subclass and mask out all commands except the ones you are interested in:
 
<code>
 
  from pykickstart.parser import KickstartParser
 
  from pykickstart.version import *
 
 
 
  superclass = returnClassForVersion()
 
 
 
  class VNCHandlers(superclass):
 
      def __init__(self, mapping={}):
 
        superclass.__init__(self, mapping=mapping)
 
        self.maskAllExcept(["vnc"])
 
 
 
  ksparser = KickstartParser(VNCHandlers())
 
  ksparser.readKickstart("ks.cfg")
 
</code>
 
Here, we make use of the BaseHandler.maskAllExcept method.  This method
 
blanks out the handler for every command except the ones given in the
 
list.  Note that the commands are specified by their string
 
representation, not by object reference.  We must also be careful when
 
creating the VNCHandler class to make sure it is a subclass.  Here, we use
 
the default DEVEL syntax version handler as the superclass.
 
 
 
You can then check the results by examining the attributes of
 
ksparser.handler.vnc.
 
 
 
==== Customized handlers ====
 
In other cases, you may want to include some customized behavior in your
 
kickstart handlers.  Due to the use of the commandMap in version.py, this
 
is not as straightforward as it should be.  Including specialized behavior
 
for a single handler involves a fairly large amount of code, but
 
specializing the behavior for all handlers does not require much more
 
overhead.
 
<code>
 
  import pykickstart.commands as commands
 
  from pykickstart.handlers.control import commandMap
 
  from pykickstart.version import *
 
 
 
  class Bootloader(commands.bootloader.FC4_Bootloader):
 
      def parse(self, args):
 
        commands.bootloader.FC4_Bootloader.parse(self, args)
 
        print "bootloader location = %s" % self.location
 
 
 
  commandMap[DEVEL]["bootloader"] = Bootloader
 
 
 
  superclass = returnClassForVersion()
 
 
 
  class KSHandlers(superclass):
 
      def __init__(self, mapping={}):
 
          superclass.__init__(self, mapping=commandMap[DEVEL])
 
 
 
  ksparser = KickstartParser(KSHandlers())
 
  ksparser.readKickstart("ks.cfg")
 
</code>
 
First, we must create a new class for the specialized command object.
 
This class does not have to be a subclass of an already existing handler,
 
but that would require fully writing the parse, __str__, and __init__
 
methods.  Instead of doing that, we just subclass it from the latest
 
version of Bootloader.
 
 
 
We then import the existing commandMap, overriding the entry for the
 
"bootloader" command with our own new class.  We also have to create a
 
special BaseHandler subclass, though here it doesn't do very much.  Its
 
only purpose is to deal with our new commandMap.  By default, the
 
pykickstart internals will use the commandMap in
 
pykickstart.handlers.control.  Since we've modified the mapping, we need
 
to tell pykickstart to use it.
 
 
 
It used to be possible to force the handlers, but now it makes more sense
 
to just create a BaseHandler subclass and stick any attributes you need
 
access to into that object.  They can then be accessed via the
 
self.handler attribute in any command object.  For instance, a handler
 
could be created like so:
 
<code>
 
  class SpecialHandler(superclass):
 
      def __init__(self, mapping={}):
 
        superclass.__init__(self, mapping=mapping)
 
 
 
        # special data we want to access in command objects
 
        self.skipSteps = []
 
        self.showSteps = []
 
        self.ksID = 10000
 
</code>
 
 
 
==== Adding a new command ====
 
Adding a new command to pykickstart is only slightly more complicated than
 
customizing a handler.  Here, we create a new hypothetical "confirm"
 
command.  If we were to add this into anaconda as well, it might do
 
something such as tell the installer to stop at the confirmation screen
 
and wait for input.
 
<code>
 
  from pykickstart.base import *
 
  from pykickstart.handlers.control import commandMap
 
  from pykickstart.errors import *
 
  from pykickstart.parser import KickstartParser
 
  from pykickstart.version import *
 
 
 
  class F7_Confirm(KickstartCommand):
 
      def __init__(self, writePriority=0, confirm=False):
 
          KickstartCommand.__init__(self, writePriority)
 
          self.confirm = confirm
 
 
 
      def __str__(self):
 
          if self.confirm:
 
              return "confirm\n"
 
          else:
 
              return ""
 
 
 
      def parse(self, args):
 
          if len(args) > 0:
 
              raise KickstartValueError, formatErrorMsg(self.lineno, msg=("Kickstart command %s does not take any arguments") % "confirm")
 
 
 
          self.confirm = True
 
 
 
  commandMap[DEVEL]["confirm"] = F7_Confirm
 
  superclass = returnClassForVersion()
 
 
 
  class KSHandlers(superclass):
 
      def __init__(self, mapping={}):
 
          superclass.__init__(self, mapping=commandMap[DEVEL])
 
 
 
  ksparser = KickstartParser(KSHandlers())
 
  ksparser.readKickstart("ks.cfg")
 
  print ksparser.handler.confirm.confim
 
</code>
 
 
 
Notice how the command object is subclassed from the base object,
 
KickstartCommand.  Its name also has a version number at the beginning.
 
While all command object names in pykickstart take the form
 
Version_CommandName, this is not strictly necessary.  Also note how
 
F7_Confirm.__init__ takes a "confirm" keyword argument.  All publicly
 
available attributes should be accepted like this for convenience.
 
 
 
==== Adding a new version ====
 
While multiple version support is one of the major features of
 
pykickstart, adding a new version is more complicated than can be shown in
 
a simple example.  The basic requirements would involve first creating a
 
new handler along the lines of those in handlers/*.py, adding new entries
 
to the commandMap and dataMap structures laying out exactly which commands
 
are supported, and then duplicating most of the code in version.py to add
 
the new version number and proper imports.
 
 
 
==== Adding a new section ====
 
Currently, there is no simple way to add a new section to the kickstart
 
file.  This requires adding more code to _stateMachine as well as
 
additional states.  _stateMachine is not set up to do this sort of thing
 
easily.
 

Latest revision as of 17:21, 14 May 2020

The pykickstart programmer's guide has moved to the pykickstart GitHub repository