Don't need to take a version argument since the Parser can get that from the
[pykickstart.git] / pykickstart / parser.py
blob5522161c0bbe15f5e8dfbf512a529a2b26ff3e4f
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.
15 """
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.
25 """
27 import shlex
28 import sys
29 import string
30 from copy import copy
31 from optparse import *
33 from constants import *
34 from errors import *
35 from options import *
36 from version import *
38 from rhpl.translate import _
39 import rhpl.translate as translate
41 translate.textdomain("pykickstart")
43 STATE_END = 0
44 STATE_COMMANDS = 1
45 STATE_PACKAGES = 2
46 STATE_SCRIPT_HDR = 3
47 STATE_PRE = 4
48 STATE_POST = 5
49 STATE_TRACEBACK = 6
51 ###
52 ### SCRIPT HANDLING
53 ###
54 class Script:
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.
60 """
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
69 environment or not?
70 interp -- The program that should be used to interpret this
71 script.
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.
76 """
77 self.script = string.join(script, "")
78 self.interp = interp
79 self.inChroot = inChroot
80 self.logfile = logfile
81 self.errorOnFail = errorOnFail
82 self.type = type
84 def __str__(self):
85 """Return a string formatted for output to a kickstart file."""
86 if self.type == KS_SCRIPT_PRE:
87 retval = '\n%pre'
88 elif self.type == KS_SCRIPT_POST:
89 retval = '\n%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
99 if self.errorOnFail:
100 retval += " --erroronfail"
102 return retval + "\n%s" % self.script
106 ## PACKAGE HANDLING
108 class Packages:
109 """A class representing the %packages section of the kickstart file."""
110 def __init__(self):
111 """Create a new Packages instance. Instance attributes:
113 addBase -- Should the Base group be installed even if it is
114 not specified?
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
118 symbol.
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
126 %packages section.
128 self.addBase = True
129 self.default = False
130 self.excludedList = []
131 self.excludeDocs = False
132 self.groupList = []
133 self.handleMissing = KS_MISSING_PROMPT
134 self.packageList = []
136 def __str__(self):
137 """Return a string formatted for output to a kickstart file."""
138 pkgs = ""
140 if not self.default:
141 for grp in self.groupList:
142 pkgs += "@%s\n" % grp
144 for pkg in self.packageList:
145 pkgs += "%s\n" % pkg
147 for pkg in self.excludedList:
148 pkgs += "-%s\n" % pkg
150 if pkgs == "":
151 return ""
153 retval = "\n%packages"
155 if self.default:
156 retval += " --default"
157 if self.excludeDocs:
158 retval += " --excludedocs"
159 if not self.addBase:
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.
170 for pkg in pkgList:
171 stripped = pkg.strip()
173 if stripped[0] == "@":
174 self.groupList.append(stripped[1:])
175 elif stripped[0] == "-":
176 self.excludedList.append(stripped[1:])
177 else:
178 self.packageList.append(stripped)
182 ### PARSER
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
189 overridden.
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
199 encountered.
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
205 will be executed.
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
213 self._reset()
215 self.version = self.handler.version
217 def _reset(self):
218 """Reset the internal variables of the state machine for a new kickstart file."""
219 self._state = STATE_COMMANDS
220 self._script = None
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() == "":
229 return
231 s = Script (self._script["body"], self._script["interp"],
232 self._script["chroot"], self._script["log"],
233 self._script["errorOnFail"], self._script["type"])
235 if self.handler:
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
241 if necessary.
243 if self.handler:
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.
251 if self.handler:
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",
265 default=False)
266 op.add_option("--ignoredeps", dest="resolveDeps", action="store_false",
267 deprecated=FC4)
268 op.add_option("--resolvedeps", dest="resolveDeps", action="store_true",
269 deprecated=FC4)
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
279 else:
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",
292 default=False)
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",
301 default=False)
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.
313 lineno = 0
314 needLine = True
316 while True:
317 if needLine:
318 try:
319 line = provideLineFn()
320 except StopIteration:
321 break
323 lineno += 1
324 needLine = False
326 # At the end of an included file
327 if line == "" and self._includeDepth > 0:
328 break
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)
339 needLine = True
340 continue
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)
352 else:
353 args = None
354 else:
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:
360 needLine = True
361 continue
363 if not args[1]:
364 raise KickstartParseError, formatErrorMsg(lineno)
365 else:
366 self._includeDepth += 1
368 try:
369 self.readKickstart (args[1], reset=False)
370 except IOError:
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:
375 raise
377 self._includeDepth -= 1
378 needLine = True
379 continue
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)
392 else:
393 needLine = True
395 if self.errorsAreFatal:
396 self.handleCommand(lineno, args)
397 else:
398 try:
399 self.handleCommand(lineno, args)
400 except Exception, msg:
401 print 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":
409 needLine = True
411 if self.errorsAreFatal:
412 self.handlePackageHdr (lineno, args)
413 else:
414 try:
415 self.handlePackageHdr (lineno, args)
416 except Exception, msg:
417 print 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)
422 else:
423 needLine = True
424 self.addPackages (string.rstrip(line))
426 elif self._state == STATE_SCRIPT_HDR:
427 needLine = True
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)
449 else:
450 try:
451 self.handleScriptHdr (lineno, args)
452 except Exception, msg:
453 print 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.
458 self.addScript()
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
464 # state.
465 self.addScript()
466 self._state = STATE_COMMANDS
467 else:
468 # Otherwise just add to the current script body.
469 self._script["body"].append(line)
470 needLine = True
472 elif self._state == STATE_END:
473 break
475 def readKickstartFromString (self, str, reset=True):
476 """Process a kickstart file, provided as the string str."""
477 if reset:
478 self._reset()
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."""
485 if reset:
486 self._reset()
488 fh = open(file)
489 self._stateMachine (lambda: fh.readline())
490 fh.close()