2 # parser.py: Kickstart file parser.
4 # Chris Lumens <clumens@redhat.com>
6 # Copyright 2005, 2006 Red Hat, Inc.
8 # This software may be freely redistributed under the terms of the GNU
9 # general public license.
11 # You should have received a copy of the GNU General Public License
12 # along with this program; if not, write to the Free Software
13 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
16 Main kickstart file processing module.
18 This module exports several important classes:
20 Script - Representation of a single %pre, %post, or %traceback script.
22 Packages - Representation of the %packages section.
24 KickstartParser - The kickstart file parser state machine.
31 from optparse
import *
33 from constants
import *
38 from rhpl
.translate
import _
39 import rhpl
.translate
as translate
41 translate
.textdomain("pykickstart")
55 """A class representing a single kickstart script. If functionality beyond
56 just a data representation is needed (for example, a run method in
57 anaconda), Script may be subclassed. Although a run method is not
58 provided, most of the attributes of Script have to do with running the
59 script. Instances of Script are held in a list by the Version object.
61 def __init__(self
, script
, interp
= "/bin/sh", inChroot
= False,
62 logfile
= None, errorOnFail
= False, type = KS_SCRIPT_PRE
):
63 """Create a new Script instance. Instance attributes:
65 errorOnFail -- If execution of the script fails, should anaconda
66 stop, display an error, and then reboot without
67 running any other scripts?
68 inChroot -- Does the script execute in anaconda's chroot
70 interp -- The program that should be used to interpret this
72 logfile -- Where all messages from the script should be logged.
73 script -- A string containing all the lines of the script.
74 type -- The type of the script, which can be KS_SCRIPT_* from
75 pykickstart.constants.
77 self
.script
= string
.join(script
, "")
79 self
.inChroot
= inChroot
80 self
.logfile
= logfile
81 self
.errorOnFail
= errorOnFail
85 """Return a string formatted for output to a kickstart file."""
86 if self
.type == KS_SCRIPT_PRE
:
88 elif self
.type == KS_SCRIPT_POST
:
90 elif self
.type == KS_SCRIPT_TRACEBACK
:
91 retval
= '\n%traceback'
93 if self
.interp
!= "/bin/sh" and self
.interp
!= "":
94 retval
+= " --interpreter=%s" % self
.interp
95 if self
.type == KS_SCRIPT_POST
and not self
.inChroot
:
96 retval
+= " --nochroot"
97 if self
.logfile
!= None:
98 retval
+= " --logfile %s" % self
.logfile
100 retval
+= " --erroronfail"
102 return retval
+ "\n%s" % self
.script
109 """A class representing the %packages section of the kickstart file."""
111 """Create a new Packages instance. Instance attributes:
113 addBase -- Should the Base group be installed even if it is
115 default -- Should the default package set be selected?
116 excludedList -- A list of all the packages marked for exclusion in
117 the %packages section, without the leading minus
119 excludeDocs -- Should documentation in each package be excluded?
120 groupList -- A list of all the groups specified in the %packages
121 section, without the leading @ symbol.
122 handleMissing -- If unknown packages are specified in the %packages
123 section, should it be ignored or not? Values can
124 be KS_MISSING_* from pykickstart.constants.
125 packageList -- A list of all the packages specified in the
130 self
.excludedList
= []
131 self
.excludeDocs
= False
133 self
.handleMissing
= KS_MISSING_PROMPT
134 self
.packageList
= []
137 """Return a string formatted for output to a kickstart file."""
141 for grp
in self
.groupList
:
142 pkgs
+= "@%s\n" % grp
144 for pkg
in self
.packageList
:
147 for pkg
in self
.excludedList
:
148 pkgs
+= "-%s\n" % pkg
153 retval
= "\n%packages"
156 retval
+= " --default"
158 retval
+= " --excludedocs"
160 retval
+= " --nobase"
161 if self
.handleMissing
== KS_MISSING_IGNORE
:
162 retval
+= " --ignoremissing"
164 return retval
+ "\n" + pkgs
166 def add (self
, pkgList
):
167 """Given a list of lines from the input file, strip off any leading
168 symbols and add the result to the appropriate list.
171 stripped
= pkg
.strip()
173 if stripped
[0] == "@":
174 self
.groupList
.append(stripped
[1:])
175 elif stripped
[0] == "-":
176 self
.excludedList
.append(stripped
[1:])
178 self
.packageList
.append(stripped
)
184 class KickstartParser
:
185 """The kickstart file parser class as represented by a basic state
186 machine. To create a specialized parser, make a subclass and override
187 any of the methods you care about. Methods that don't need to do
188 anything may just pass. However, readKickstart should never be
191 def __init__ (self
, handler
, followIncludes
=True, errorsAreFatal
=True,
192 missingIncludeIsFatal
=True):
193 """Create a new KickstartParser instance. Instance attributes:
195 errorsAreFatal -- Should errors cause processing to halt, or
196 just print a message to the screen? This
197 is most useful for writing syntax checkers
198 that may want to continue after an error is
200 followIncludes -- If %include is seen, should the included
201 file be checked as well or skipped?
202 handler -- An instance of a BaseHandler subclass. If
203 None, the input file will still be parsed
204 but no data will be saved and no commands
206 missingIncludeIsFatal -- Should missing include files be fatal, even
207 if errorsAreFatal is False?
209 self
.errorsAreFatal
= errorsAreFatal
210 self
.followIncludes
= followIncludes
211 self
.handler
= handler
212 self
.missingIncludeIsFatal
= missingIncludeIsFatal
215 self
.version
= self
.handler
.version
218 """Reset the internal variables of the state machine for a new kickstart file."""
219 self
._state
= STATE_COMMANDS
221 self
._includeDepth
= 0
223 def addScript (self
):
224 """Create a new Script instance and add it to the Version object. This
225 is called when the end of a script section is seen and may be
226 overridden in a subclass if necessary.
228 if string
.join(self
._script
["body"]).strip() == "":
231 s
= Script (self
._script
["body"], self
._script
["interp"],
232 self
._script
["chroot"], self
._script
["log"],
233 self
._script
["errorOnFail"], self
._script
["type"])
236 self
.handler
.scripts
.append(s
)
238 def addPackages (self
, line
):
239 """Add the single package, exclude, or group into the Version's
240 Packages instance. This method may be overridden in a subclass
244 self
.handler
.packages
.add([line
])
246 def handleCommand (self
, lineno
, args
):
247 """Given the list of command and arguments, call the Version's
248 dispatcher method to handle the command. This method may be
249 overridden in a subclass if necessary.
252 self
.handler
.dispatcher(args
[0], args
[1:], lineno
)
254 def handlePackageHdr (self
, lineno
, args
):
255 """Process the arguments to the %packages header and set attributes
256 on the Version's Packages instance appropriate. This method may be
257 overridden in a subclass if necessary.
259 op
= KSOptionParser(lineno
=lineno
, version
=self
.version
)
260 op
.add_option("--excludedocs", dest
="excludedocs", action
="store_true",
261 default
=False, deprecated
=FC6
)
262 op
.add_option("--ignoremissing", dest
="ignoremissing",
263 action
="store_true", default
=False)
264 op
.add_option("--nobase", dest
="nobase", action
="store_true",
266 op
.add_option("--ignoredeps", dest
="resolveDeps", action
="store_false",
268 op
.add_option("--resolvedeps", dest
="resolveDeps", action
="store_true",
270 op
.add_option("--default", dest
="defaultPackages", action
="store_true",
271 default
=False, introduced
=F7
)
273 (opts
, extra
) = op
.parse_args(args
=args
[1:])
275 self
.handler
.packages
.excludeDocs
= opts
.excludedocs
276 self
.handler
.packages
.addBase
= not opts
.nobase
277 if opts
.ignoremissing
:
278 self
.handler
.packages
.handleMissing
= KS_MISSING_IGNORE
280 self
.handler
.packages
.handleMissing
= KS_MISSING_PROMPT
282 if opts
.defaultPackages
:
283 self
.handler
.packages
.default
= True
285 def handleScriptHdr (self
, lineno
, args
):
286 """Process the arguments to a %pre/%post/%traceback header for later
287 setting on a Script instance once the end of the script is found.
288 This method may be overridden in a subclass if necessary.
290 op
= KSOptionParser(lineno
=lineno
, version
=self
.version
)
291 op
.add_option("--erroronfail", dest
="errorOnFail", action
="store_true",
293 op
.add_option("--interpreter", dest
="interpreter", default
="/bin/sh")
294 op
.add_option("--log", "--logfile", dest
="log")
296 if args
[0] == "%pre" or args
[0] == "%traceback":
297 self
._script
["chroot"] = False
298 elif args
[0] == "%post":
299 self
._script
["chroot"] = True
300 op
.add_option("--nochroot", dest
="nochroot", action
="store_true",
303 (opts
, extra
) = op
.parse_args(args
=args
[1:])
305 self
._script
["interp"] = opts
.interpreter
306 self
._script
["log"] = opts
.log
307 self
._script
["errorOnFail"] = opts
.errorOnFail
308 if hasattr(opts
, "nochroot"):
309 self
._script
["chroot"] = not opts
.nochroot
311 def _stateMachine (self
, provideLineFn
):
312 # For error reporting.
319 line
= provideLineFn()
320 except StopIteration:
326 # At the end of an included file
327 if line
== "" and self
._includeDepth
> 0:
330 # Don't eliminate whitespace or comments from scripts.
331 if line
.isspace() or (line
!= "" and line
.lstrip()[0] == '#'):
332 # Save the platform for s-c-kickstart, though.
333 if line
[:10] == "#platform=" and self
._state
== STATE_COMMANDS
:
334 self
.handler
.platform
= line
[11:]
336 if self
._state
in [STATE_PRE
, STATE_POST
, STATE_TRACEBACK
]:
337 self
._script
["body"].append(line
)
342 # We only want to split the line if we're outside of a script,
343 # as inside the script might involve some pretty weird quoting
344 # that shlex doesn't understand.
345 if self
._state
in [STATE_PRE
, STATE_POST
, STATE_TRACEBACK
]:
346 # Have we found a state transition? If so, we still want
347 # to split. Otherwise, args won't be set but we'll fall through
348 # all the way to the last case.
349 if line
!= "" and string
.split(line
.lstrip())[0] in \
350 ["%post", "%pre", "%traceback", "%include", "%packages"]:
351 args
= shlex
.split(line
)
355 args
= shlex
.split(line
)
357 if args
and args
[0] == "%include":
358 # This case comes up primarily in ksvalidator.
359 if not self
.followIncludes
:
364 raise KickstartParseError
, formatErrorMsg(lineno
)
366 self
._includeDepth
+= 1
369 self
.readKickstart (args
[1], reset
=False)
371 # Handle the include file being provided over the
372 # network in a %pre script. This case comes up in the
373 # early parsing in anaconda.
374 if self
.missingIncludeIsFatal
:
377 self
._includeDepth
-= 1
381 if self
._state
== STATE_COMMANDS
:
382 if not args
and self
._includeDepth
== 0:
383 self
._state
= STATE_END
384 elif args
[0] in ["%pre", "%post", "%traceback"]:
385 self
._state
= STATE_SCRIPT_HDR
386 elif args
[0] == "%packages":
387 self
._state
= STATE_PACKAGES
388 elif args
[0][0] == '%':
389 # This error is too difficult to continue from, without
390 # lots of resync code. So just print this one and quit.
391 raise KickstartParseError
, formatErrorMsg(lineno
)
395 if self
.errorsAreFatal
:
396 self
.handleCommand(lineno
, args
)
399 self
.handleCommand(lineno
, args
)
400 except Exception, msg
:
403 elif self
._state
== STATE_PACKAGES
:
404 if not args
and self
._includeDepth
== 0:
405 self
._state
= STATE_END
406 elif args
[0] in ["%pre", "%post", "%traceback"]:
407 self
._state
= STATE_SCRIPT_HDR
408 elif args
[0] == "%packages":
411 if self
.errorsAreFatal
:
412 self
.handlePackageHdr (lineno
, args
)
415 self
.handlePackageHdr (lineno
, args
)
416 except Exception, msg
:
418 elif args
[0][0] == '%':
419 # This error is too difficult to continue from, without
420 # lots of resync code. So just print this one and quit.
421 raise KickstartParseError
, formatErrorMsg(lineno
)
424 self
.addPackages (string
.rstrip(line
))
426 elif self
._state
== STATE_SCRIPT_HDR
:
428 self
._script
= {"body": [], "interp": "/bin/sh", "log": None,
429 "errorOnFail": False}
431 if not args
and self
._includeDepth
== 0:
432 self
._state
= STATE_END
433 elif args
[0] == "%pre":
434 self
._state
= STATE_PRE
435 self
._script
["type"] = KS_SCRIPT_PRE
436 elif args
[0] == "%post":
437 self
._state
= STATE_POST
438 self
._script
["type"] = KS_SCRIPT_POST
439 elif args
[0] == "%traceback":
440 self
._state
= STATE_TRACEBACK
441 self
._script
["type"] = KS_SCRIPT_TRACEBACK
442 elif args
[0][0] == '%':
443 # This error is too difficult to continue from, without
444 # lots of resync code. So just print this one and quit.
445 raise KickstartParseError
, formatErrorMsg(lineno
)
447 if self
.errorsAreFatal
:
448 self
.handleScriptHdr (lineno
, args
)
451 self
.handleScriptHdr (lineno
, args
)
452 except Exception, msg
:
455 elif self
._state
in [STATE_PRE
, STATE_POST
, STATE_TRACEBACK
]:
456 if line
== "" and self
._includeDepth
== 0:
457 # If we're at the end of the kickstart file, add the script.
459 self
._state
= STATE_END
460 elif args
and args
[0] in ["%pre", "%post", "%traceback", "%packages"]:
461 # Otherwise we're now at the start of the next section.
462 # Figure out what kind of a script we just finished
463 # reading, add it to the list, and switch to the initial
466 self
._state
= STATE_COMMANDS
468 # Otherwise just add to the current script body.
469 self
._script
["body"].append(line
)
472 elif self
._state
== STATE_END
:
475 def readKickstartFromString (self
, str, reset
=True):
476 """Process a kickstart file, provided as the string str."""
480 i
= iter(str.splitlines(True))
481 self
._stateMachine
(lambda: i
.next())
483 def readKickstart (self
, file, reset
=True):
484 """Process a kickstart file, given by the filename file."""
489 self
._stateMachine
(lambda: fh
.readline())