Allow passing a command mapping to BaseHandler.__init__ for overriding
[pykickstart.git] / docs / programmers-guide
bloba9fe0f174e0e496f05db87ba870031c93543aad0
3                         pykickstart Programmer's Guide
5                                by Chris Lumens
7                               January 17, 2007
10 Introduction
11 ============
12 pykickstart is a Python library for manipulating kickstart files.  It
13 contains a common data representation, a parser, and a writer.  This
14 library aims to be useful for all Python programs that need to work with
15 kickstart files.  The two most obvious examples are anaconda and
16 system-config-kickstart.  It is recommended that all other tools that need
17 to use kickstart files use this library so that we can maintain equivalent
18 levels of support across all tools.
20 The kickstart file format itself has only been defined in a rather ad-hoc
21 manner.  Various documents describe the format, commands, and their
22 effects.  However, each kickstart-related program implemented its own
23 parser.  As anaconda added support for new commands and options, other
24 programs drifted farther and farther out of sync with the "official"
25 format.  This leads to the problem that valid kickstart files are not
26 accepted by all programs, or that programs will strip out options it
27 doesn't understand so that the input and output files do not match.
29 pykickstart is an effort to correct this.  It was originally designed to
30 be a common code base for anaconda and system-config-kickstart, so making
31 the code generic and easily extendable were top priorities.  Another
32 priority was to formalize the currently recognized grammar in an easily
33 understood parser so that files that used to work would continue to.  I
34 believe these goals have been met.
36 pykickstart also understands all the various versions of the kickstart syntax
37 that have been around.  Various releases of Red Hat Linux, Red Hat Enterprise
38 Linux, and Fedora Core have had slightly different versions.  For the most
39 part, the basic syntax has stayed the same.  However, different commands have
40 come and gone and different options have been supported on those commands.
41 pykickstart allows specifying which version of kickstart syntax you want
42 to support for reading and writing, allowing you to use one code base to
43 deal with the full range of kickstart files.
45 This document will cover how to use pykickstart in your programs and how to
46 extend the basic parser to get customized behavior.  It includes a
47 description of the important classes and several working examples.
50 Getting Started
51 ===============
52 Before diving into the full documentation, it is useful to see an example
53 of how simple it is to use the default pykickstart in your programs.  Here
54 is a code snippet that imports the required classes, parses a kickstart
55 file, and leaves the results in the common data format.
57         #!/usr/bin/python
58         from pykickstart.parser import *
59         from pykickstart.version import makeHandler
61         kshandlers = makeHandler()
62         ksparser = KickstartParser(hsandlers)
63         ksparser.readKickstart("ks.cfg")
65 The call to makeHandler() creates a new kickstart handler object for the
66 specified version.  By default, it creates one for the latest supported
67 syntax version.  The call to KickstartParser() creates a new parser using
68 the handler object for dealing with individual commands.  The call to
69 readKickstart() then reads in the kickstart file and sets values in the
70 handler.
72 After this call, you don't need ksparser anymore but you will want to hold
73 on to kshandlers if you are interested in the contents of the file.  If
74 you want to do some manipulations and then write out the results to a new
75 file, that's as simple as:
77         outfile = open("out.cfg", 'w")
78         outfile.write(kshandlers.__str__())
79         outfile.close()
82 Classes
83 =======
84 The important classes that make up pykickstart are spread across a handful
85 of files.  This section includes a brief outline of the contents of those
86 classes.  For more thorough documentation, refer to the python doc strings
87 throughout pykickstart.  In python, you can view these docs strings like
88 so:
90         >>> from pykickstart import parser
91         >>> help(parser)
92         >>> help(parser.KickstartParser)
94 constants.py
95 ------------
96 This file includes no classes, though it does include several important
97 constants representing various things in a kickstart handler class.  You
98 should import its contents like so:
100         from pykickstart.constants import *
103 *** EVERYTHING UNDER HERE IS CURRENTLY OUT OF DATE AND SLOWLY BEING
104 *** REWRITTEN.
106 data.py
107 -------
108 This file contains the classes that make up the common data
109 representation.  The center of the data format is the KickstartData class,
110 which contains many attributes representing the values from the input
111 file.  When a new KickstartData is instantiated, these attributes are set
112 to appropriate default values.  Some attributes are a simple string or
113 boolean, while some are a list of other classes or dictionaries.  For the
114 most part, each lines up with a single kickstart command.
116 The KickstartLogVolData, KickstartNetworkData, KickstartPartData,
117 KickstartRaidData, and KickstartVolGroupData classes are contained as list
118 elements in KickstartData.  Each corresponds to a command that may appear
119 multiple times.  They exist as separate classes so that attributes are
120 guaranteed to exist.
122 There are three different types of scripts - pre, post, and traceback -
123 but all are represented in the kickstart data by a single list.  The
124 Script class is contained in parser.py and contains an attribute that may
125 be used to discriminate between classes.
127 The install package list, excluded package list, and install groups list
128 are kept as separate.  Excluded packages have the leading "-" stripped
129 off, and groups have the leading "@" stripped.
131 See the class reference at the end of this documentation for a brief
132 explanation of useful functions in each class.
134 parser.py
135 ---------
136 This file represents the bulk of pykickstart code.  At its core is the
137 KickstartParser class, which is essentially a state machine.  There is one
138 state for each of the sections in a kickstart file, plus some specialized
139 ones to make the parser work.  The readKickstart method is the entry point
140 into this class and is designed to be as generic as possible.  You should
141 never have to override this method in a subclass.  All the other methods
142 handle a change into a specific state and may be overridden in a
143 superclass.
145 If the KickstartParser encounters an error while reading your input file,
146 it will raise a KickstartParseError with the line in question.  Examples
147 of errors include bad options given to section headers, include files not
148 existing, or headers given for sections that are not recognized (for
149 instance, typos).
151 Error messages should call the formatErrorMsg function to be properly
152 formatted before being sent into an exception.  A properly formatted error
153 message includes the line number in the kickstart file where the problem
154 occurred and optionally, a more descriptive message.
156 The other major class within parser.py is KickstartHeaders.  This makes up
157 the largest amount of the code and also does the most work as it deals
158 with processing all the options on all the kickstart commands.
159 KickstartHandlers.handlers is a dictionary mapping command names to
160 handling methods.  KickstartParser takes the current command and
161 dispatches the correct method based on this mapping.  If the command name
162 maps to None, no method is called and no error is issued, a handy feature
163 which will be discussed later in this documentation.
165 Each of the handlers makes use of Python's OptionParser module to parse
166 command arguments and set values in the KickstartData object.  For this
167 reason, any subclass of KickstartHandlers must call the superclass's
168 handler to ensure that the KickstartData is correct.  Our option parsing
169 code allows commands to be marked as deprecated, which causes a message to
170 be generated and logged by anaconda.  It also allows marking options as
171 deprecated or required.
173 There are a few other minor points to note about KickstartParser and
174 KickstartHandlers.  When creating a KickstartParser object, you can set
175 the followIncludes attribute to False if you do not wish for include files
176 to be looked up and parsed as well.  There are several instances when this
177 is handy.  The order of instantiation for these objects is fixed, as each
178 object requires certain ones created before it.  KickstartData must be
179 first, as KickstartHandlers and KickstartParser require it.
180 KickstartHandlers must come second, and then finally KickstartParser.
181 Note that you can pass None in for kshandlers in the special case if you
182 do not care about handling any commands at all.  As we will see in the
183 next section, this is useful in one special case.
185 writer.py
186 ---------
187 This file contains the class that makes up the Kickstart writer.  The job
188 of this class is to take a KickstartData object and convert it into a
189 string.  This string should be a valid kickstart file that can then be
190 used in any program.  Ideally, it should be the same as the input file
191 though the order of options may have been shifted, as well as other
192 cosmetic differences.
194 It is important to note that KickstartWriter.write returns a string, but
195 does no file output.  KickstartWriter is laid out similarly to
196 KickstartParser.  It consists of one handler per command plus special ones
197 for scripts and packages, plus a list specifying the order these handlers
198 should be called in.  It is possible to add your own handlers to
199 KickstartWriter if you are extending kickstart with special commands, as
200 will be discussed in the next section.
202 See the class reference at the end of this documentation for a brief
203 explanation of useful functions in each class.
206 Extending pykickstart
207 =====================
208 By default, pykickstart reads in a kickstart file and sets values in a
209 KickstartData object.  This is useful for some applications, but not all.
210 anaconda in particular has some odd requirements for kickstart so it will
211 be our basis for examples on extending pykickstart.
213 Only paying attention to one command
214 ------------------------------------
215 Sometimes, you only want to take action on a single kickstart command and
216 don't care about any of the others.  anaconda, for instance, supports a
217 vnc command that needs to be processed well before any of the other
218 commands.  Remember from earlier that any command mapping to None in the
219 handlers is skipped.  That makes this fairly easy, then.  All we need to
220 do is make a specialized KickstartHandlers subclass and hook this into a
221 regular parser:
223         from pykickstart.data import *
224         from pykickstart.parser import *
226         class VNCHandlers(KickstartHandlers):
227                 def __init__ (self, ksdata):
228                         KickstartHandlers.__init__(self, ksdata)
229                         self.resetHandlers()
230                         self.handlers["vnc"] = self.doVnc
232         ksdata = KickstartData()
233         kshandlers = VNCHandlers(ksdata)
234         ksparser = KickstartParser(ksdata, kshandlers)
235         ksparser.readKickstart("ks.cfg")
237 Here, we make use of the KickstartHandlers.resetHandlers method.  This
238 method blanks out every entry in the handlers dictionary.  We then set the
239 vnc command handler to the default value, which you can easily get from
240 checking out the pykickstart source.  Then, make an instance of our new
241 class and pass this to KickstartParser().  When readKickstart is called,
242 the file will be parsed as usual but only the vnc command will ever be
243 handled.
245 You can then check the results by examining ksdata.vnc.
247 Customized handlers
248 -------------------
249 In other cases, you may want to include some customized behavior in your
250 kickstart handlers.  In this case, you'll still want to create a subclass
251 of KickstartHandlers, though you will need to make sure your handler calls
252 the superclass still.
254         from pykickstart.data import *
255         from pykickstart.parser import *
257         class KSHandlers(KickstartHandlers):
258                 def doBootloader (self, args):
259                         KickstartHandlers.doBootloader(self, args)
260                         print "bootloader location = %s" % self.ksdata.bootloader["location"]
262         ksdata = KickstartData()
263         kshandlers = VNCHandlers(ksdata)
264         ksparser = KickstartParser(ksdata, kshandlers)
265         ksparser.readKickstart("ks.cfg")
267 This example is very simple, but you can still see what would be required
268 for complex cases.  Your handler needs to accept an args argument, which
269 is a list of arguments provided to the command.  Then, make sure to call
270 the superclass's method of the same name to set the KickstartData.
271 Finally, do your specialized behavior.
273 It is even possible to force your handlers to accept more arguments,
274 though it is slightly more complicated.  This requires making a subclass
275 of KickstartParser in addition to KickstartHandlers.
277         from pykickstart.data import *
278         from pykickstart.parser import *
280         class KSHandlers(KickstartHandlers):
281                 def doBootloader (self, userarg, args):
282                         KickstartHandlers.doBootloader(self, args)
283                         print "%s bootloader location = %s" % (userarg, self.ksdata.bootloader["location"])
285                 ...
287         class KSParser(KickstartParser):
288                 def __init__ (self, ksdata, kshandlers, userarg):
289                         self.userarg = userarg
290                         KickstartParser.__init__(self, ksdata, kshandlers)
292                 def handleCommand (self, cmd, args):
293                         if not self.handler:
294                                 return
296                         if not self.handler.handlers.has_key(cmd):
297                                 raise KickstartParseError, (cmd + " " + string.join(args))
298                         else:
299                                 if self.handler.handlers[cmd] != None:
300                                         self.handler.currentCmd = cmd
301                                         self.handler.handlers[cmd](self.userarg,
302 args)
304         ksdata = KickstartData()
305         kshandlers = VNCHandlers(ksdata)
306         ksparser = KSParser(ksdata, kshandlers, "note: ")
307         ksparser.readKickstart("ks.cfg")
309 Let's examine this example a little more closely.  First, you need to
310 create a subclass of KickstartParser whose __init__ method stores your
311 argument.  Then you also need to override handleCommand.  In the
312 overridden method, the only difference is that on the last line you'll
313 need to pass your argument.
315 In your subclassed KickstartHandlers, you will need to make sure every
316 handler accepts your new argument.  You could possibly get around this
317 requirement by further modifying handleCommand to strip off your argument
318 for commands that do not accept it.  However, I'm not demonstrating that
319 here.  Then, make sure to call the superclass's handler method without any
320 additional arguments.
322 Adding a new command
323 --------------------
324 Adding support for a new command is fairly straightforward.  You'll need
325 to create a KickstartHandlers and KickstartWriter subclass and add your
326 methods to the handler lists.
328         from pykickstart.data import *
329         from pykickstart.parser import *
330         from pykickstart.writer import *
332         class SpecialHandlers(KickstartHandlers):
333                 def __init__ (self, ksdata):
334                         KickstartHandlers.__init__(self, ksdata)
335                         self.handlers["log"] = self.doLog
337                 def doLog (self, args):
338                         op = KSOptionParser()
339                         op.add_option("--level", dest="level")
340                         op.add_option("--host", dest="host")
342                         (opts, extra) = op.parse_args(args=args)
344                         self.ksdata.log["level"] = getattr(opts, "level")
345                         self.ksdata.log["host"] = getattr(opts, "host")
347         class SpecialWriter(KickstartWriter):
348                 def __init__ (self, ksdata):
349                         KickstartWriter.__init__(self, ksdata)
350                         self.handlers.insert(1, self.doLog)
352                 def doLog(self):
353                         argstr = ""
355                         if self.ksdata.log["level"]:
356                                 argstr = "--level=%s" % self.ksdata.log["level"]
357                         if self.ksdata.log["host"]:
358                                 argstr = argstr + "--host=%s" % self.ksdata.log["host"]
360                         if argstr != "":
361                                 return "log %s" % argstr
362                         else
363                                 return
365         ksdata = KickstartData()
366         kshandlers = SpecialHandlers(ksdata)
367         ksparser = KickstartParser(ksdata, kshandlers)
368         ksparser.readKickstart("ks.cfg")
370 This is all fairly straightforward, with the possible exception of the
371 OptionParser stuff.  Without getting into it too much, you'll need to
372 create a new instance of KSOptionParser, and then use the Python
373 documentation on how to use it.  It's a very complex, powerful class.
374 Make sure to set the KickstartData afterwards.
376 The KickstartWriter object is also pretty simple.  Make sure your method
377 returns an empty string if there's nothing set on this option, and the
378 appropriate string otherwise.
380 Adding a new section
381 --------------------
382 Currently, there is no simple way to add a new section.  This requires
383 adding more code to readKickstart as well as additional states.
384 readKickstart is not set up to do this sort of thing easily.