2 # parser.py: Kickstart file parser.
4 # Chris Lumens <clumens@redhat.com>
6 # Copyright 2005, 2006, 2007, 2008, 2011 Red Hat, Inc.
8 # This copyrighted material is made available to anyone wishing to use, modify,
9 # copy, or redistribute it subject to the terms and conditions of the GNU
10 # General Public License v.2. This program is distributed in the hope that it
11 # will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
12 # implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13 # See the GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License along with
16 # this program; if not, write to the Free Software Foundation, Inc., 51
17 # Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat
18 # trademarks that are incorporated in the source code or documentation are not
19 # subject to the GNU General Public License and may only be used or replicated
20 # with the express permission of Red Hat, Inc.
23 Main kickstart file processing module.
25 This module exports several important classes:
27 Script - Representation of a single %pre, %post, or %traceback script.
29 Packages - Representation of the %packages section.
31 KickstartParser - The kickstart file parser state machine.
33 from collections import Iterator
39 from optparse import *
40 from urlgrabber import urlread
41 import urlgrabber.grabber as grabber
44 from errors import KickstartError, KickstartParseError, KickstartValueError, formatErrorMsg
45 from ko import KickstartObject
46 from sections import *
50 _ = lambda x: gettext.ldgettext("pykickstart", x)
53 STATE_COMMANDS = "commands"
57 def _preprocessStateMachine (lineIter):
61 # Now open an output kickstart file that we are going to write to one
63 (outF, outName) = tempfile.mkstemp("-ks.cfg", "", "/tmp")
71 # At the end of the file?
79 if not ll.startswith("%ksappend"):
83 # Try to pull down the remote file.
85 ksurl = ll.split(' ')[1]
87 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Illegal url for %%ksappend: %s") % ll)
90 url = grabber.urlopen(ksurl)
91 except grabber.URLGrabError, e:
92 raise KickstartError, formatErrorMsg(lineno, msg=_("Unable to open %%ksappend file: %s") % e.strerror)
94 # Sanity check result. Sometimes FTP doesn't catch a file
98 raise KickstartError, formatErrorMsg(lineno, msg=_("Unable to open %%ksappend file"))
100 raise KickstartError, formatErrorMsg(lineno, msg=_("Unable to open %%ksappend file"))
102 # If that worked, write the remote file to the output kickstart
103 # file in one burst. Then close everything up to get ready to
104 # read ahead in the input file. This allows multiple %ksappend
107 os.write(outF, url.read())
110 # All done - close the temp file and return its location.
114 def preprocessFromString (s):
115 """Preprocess the kickstart file, provided as the string str. This
116 method is currently only useful for handling %ksappend lines,
117 which need to be fetched before the real kickstart parser can be
118 run. Returns the location of the complete kickstart file.
120 i = iter(s.splitlines(True) + [""])
121 rc = _preprocessStateMachine (i.next)
124 def preprocessKickstart (f):
125 """Preprocess the kickstart file, given by the filename file. This
126 method is currently only useful for handling %ksappend lines,
127 which need to be fetched before the real kickstart parser can be
128 run. Returns the location of the complete kickstart file.
132 except grabber.URLGrabError, e:
133 raise KickstartError, formatErrorMsg(0, msg=_("Unable to open input kickstart file: %s") % e.strerror)
135 rc = _preprocessStateMachine (iter(fh.readlines()))
139 class PutBackIterator(Iterator):
140 def __init__(self, iterable):
141 self._iterable = iter(iterable)
156 return self._iterable.next()
161 class Script(KickstartObject):
162 """A class representing a single kickstart script. If functionality beyond
163 just a data representation is needed (for example, a run method in
164 anaconda), Script may be subclassed. Although a run method is not
165 provided, most of the attributes of Script have to do with running the
166 script. Instances of Script are held in a list by the Version object.
168 def __init__(self, script, *args , **kwargs):
169 """Create a new Script instance. Instance attributes:
171 errorOnFail -- If execution of the script fails, should anaconda
172 stop, display an error, and then reboot without
173 running any other scripts?
174 inChroot -- Does the script execute in anaconda's chroot
176 interp -- The program that should be used to interpret this
178 lineno -- The line number this script starts on.
179 logfile -- Where all messages from the script should be logged.
180 script -- A string containing all the lines of the script.
181 type -- The type of the script, which can be KS_SCRIPT_* from
182 pykickstart.constants.
184 KickstartObject.__init__(self, *args, **kwargs)
185 self.script = "".join(script)
187 self.interp = kwargs.get("interp", "/bin/sh")
188 self.inChroot = kwargs.get("inChroot", False)
189 self.lineno = kwargs.get("lineno", None)
190 self.logfile = kwargs.get("logfile", None)
191 self.errorOnFail = kwargs.get("errorOnFail", False)
192 self.type = kwargs.get("type", constants.KS_SCRIPT_PRE)
195 """Return a string formatted for output to a kickstart file."""
198 if self.type == constants.KS_SCRIPT_PRE:
200 elif self.type == constants.KS_SCRIPT_POST:
202 elif self.type == constants.KS_SCRIPT_TRACEBACK:
203 retval += '\n%traceback'
204 elif self.type == constants.KS_SCRIPT_RUN:
205 retval += '\n%runscript'
207 if self.interp != "/bin/sh" and self.interp != "":
208 retval += " --interpreter=%s" % self.interp
209 if self.type == constants.KS_SCRIPT_POST and not self.inChroot:
210 retval += " --nochroot"
211 if self.logfile != None:
212 retval += " --logfile %s" % self.logfile
214 retval += " --erroronfail"
216 if self.script.endswith("\n"):
217 if ver >= version.F8:
218 return retval + "\n%s%%end\n" % self.script
220 return retval + "\n%s\n" % self.script
222 if ver >= version.F8:
223 return retval + "\n%s\n%%end\n" % self.script
225 return retval + "\n%s\n" % self.script
232 """A class representing a single group in the %packages section."""
233 def __init__(self, name="", include=constants.GROUP_DEFAULT):
234 """Create a new Group instance. Instance attributes:
236 name -- The group's identifier
237 include -- The level of how much of the group should be included.
238 Values can be GROUP_* from pykickstart.constants.
241 self.include = include
244 """Return a string formatted for output to a kickstart file."""
245 if self.include == constants.GROUP_REQUIRED:
246 return "@%s --nodefaults" % self.name
247 elif self.include == constants.GROUP_ALL:
248 return "@%s --optional" % self.name
250 return "@%s" % self.name
252 def __cmp__(self, other):
253 if self.name < other.name:
255 elif self.name > other.name:
259 class Packages(KickstartObject):
260 """A class representing the %packages section of the kickstart file."""
261 def __init__(self, *args, **kwargs):
262 """Create a new Packages instance. Instance attributes:
264 addBase -- Should the Base group be installed even if it is
266 default -- Should the default package set be selected?
267 excludedList -- A list of all the packages marked for exclusion in
268 the %packages section, without the leading minus
270 excludeDocs -- Should documentation in each package be excluded?
271 groupList -- A list of Group objects representing all the groups
272 specified in the %packages section. Names will be
273 stripped of the leading @ symbol.
274 excludedGroupList -- A list of Group objects representing all the
275 groups specified for removal in the %packages
276 section. Names will be stripped of the leading
278 handleMissing -- If unknown packages are specified in the %packages
279 section, should it be ignored or not? Values can
280 be KS_MISSING_* from pykickstart.constants.
281 packageList -- A list of all the packages specified in the
283 instLangs -- A list of languages to install.
285 KickstartObject.__init__(self, *args, **kwargs)
289 self.excludedList = []
290 self.excludedGroupList = []
291 self.excludeDocs = False
293 self.handleMissing = constants.KS_MISSING_PROMPT
294 self.packageList = []
295 self.tpk_packageList = []
296 self.instLangs = None
299 """Return a string formatted for output to a kickstart file."""
303 grps = self.groupList
306 pkgs += "%s\n" % grp.__str__()
313 grps = self.excludedGroupList
316 pkgs += "-%s\n" % grp.__str__()
318 p = self.excludedList
321 pkgs += "-%s\n" % pkg
326 retval = "\n%packages"
329 retval += " --default"
331 retval += " --excludedocs"
333 retval += " --nobase"
334 if self.handleMissing == constants.KS_MISSING_IGNORE:
335 retval += " --ignoremissing"
337 retval += " --instLangs=%s" % self.instLangs
339 if ver >= version.F8:
340 return retval + "\n" + pkgs + "\n%end\n"
342 return retval + "\n" + pkgs + "\n"
344 def _processGroup (self, line):
346 op.add_option("--nodefaults", action="store_true", default=False)
347 op.add_option("--optional", action="store_true", default=False)
349 (opts, extra) = op.parse_args(args=line.split())
351 if opts.nodefaults and opts.optional:
352 raise KickstartValueError, _("Group cannot specify both --nodefaults and --optional")
354 # If the group name has spaces in it, we have to put it back together
356 grp = " ".join(extra)
359 self.groupList.append(Group(name=grp, include=constants.GROUP_REQUIRED))
361 self.groupList.append(Group(name=grp, include=constants.GROUP_ALL))
363 self.groupList.append(Group(name=grp, include=constants.GROUP_DEFAULT))
365 def add (self, pkgList):
366 """Given a list of lines from the input file, strip off any leading
367 symbols and add the result to the appropriate list.
369 existingExcludedSet = set(self.excludedList)
370 existingPackageSet = set(self.packageList)
371 newExcludedSet = set()
372 newPackageSet = set()
374 excludedGroupList = []
377 stripped = pkg.strip()
379 if stripped[0] == "@":
380 self._processGroup(stripped[1:])
381 elif stripped[0] == "-":
382 if stripped[1] == "@":
383 excludedGroupList.append(Group(name=stripped[2:]))
385 newExcludedSet.add(stripped[1:])
387 newPackageSet.add(stripped)
389 # Groups have to be excluded in two different ways (note: can't use
390 # sets here because we have to store objects):
391 excludedGroupNames = map(lambda g: g.name, excludedGroupList)
393 # First, an excluded group may be cancelling out a previously given
394 # one. This is often the case when using %include. So there we should
395 # just remove the group from the list.
396 self.groupList = filter(lambda g: g.name not in excludedGroupNames, self.groupList)
398 # Second, the package list could have included globs which are not
399 # processed by pykickstart. In that case we need to preserve a list of
400 # excluded groups so whatever tool doing package/group installation can
401 # take appropriate action.
402 self.excludedGroupList.extend(excludedGroupList)
404 existingPackageSet = (existingPackageSet - newExcludedSet) | newPackageSet
405 existingExcludedSet = (existingExcludedSet - existingPackageSet) | newExcludedSet
407 self.packageList = list(existingPackageSet)
408 self.excludedList = list(existingExcludedSet)
410 class TpkPackages(KickstartObject):
411 """A class representing the %tpk_packages section of the kickstart file."""
412 def __init__(self, *args, **kwargs):
413 KickstartObject.__init__(self, *args, **kwargs)
414 self.tpk_packageList = []
417 retval = "\n%tpk_packages"
421 tpk_pkgs += "%s\n" % pkg
422 return retval + "\n" +tpk_pkgs
423 def add(self, tpkPackageList):
424 tpk_PackageSet = set(self.tpk_packageList)
425 for tpk_pkg in tpkPackageList:
426 stripped = tpk_pkg.strip()
427 tpk_PackageSet.add(stripped)
428 self.tpk_packageList = list(tpk_PackageSet)
432 class KickstartParser:
433 """The kickstart file parser class as represented by a basic state
434 machine. To create a specialized parser, make a subclass and override
435 any of the methods you care about. Methods that don't need to do
436 anything may just pass. However, _stateMachine should never be
439 def __init__ (self, handler, followIncludes=True, errorsAreFatal=True,
440 missingIncludeIsFatal=True):
441 """Create a new KickstartParser instance. Instance attributes:
443 errorsAreFatal -- Should errors cause processing to halt, or
444 just print a message to the screen? This
445 is most useful for writing syntax checkers
446 that may want to continue after an error is
448 followIncludes -- If %include is seen, should the included
449 file be checked as well or skipped?
450 handler -- An instance of a BaseHandler subclass. If
451 None, the input file will still be parsed
452 but no data will be saved and no commands
454 missingIncludeIsFatal -- Should missing include files be fatal, even
455 if errorsAreFatal is False?
457 self.errorsAreFatal = errorsAreFatal
458 self.followIncludes = followIncludes
459 self.handler = handler
461 self.missingIncludeIsFatal = missingIncludeIsFatal
463 self._state = STATE_COMMANDS
464 self._includeDepth = 0
467 self.version = self.handler.version
476 """Reset the internal variables of the state machine for a new kickstart file."""
477 self._state = STATE_COMMANDS
478 self._includeDepth = 0
480 def getSection(self, s):
481 """Return a reference to the requested section (s must start with '%'s),
482 or raise KeyError if not found.
484 return self._sections[s]
486 def handleCommand (self, lineno, args):
487 """Given the list of command and arguments, call the Version's
488 dispatcher method to handle the command. Returns the command or
489 data object returned by the dispatcher. This method may be
490 overridden in a subclass if necessary.
493 self.handler.currentCmd = args[0]
494 self.handler.currentLine = self._line
495 retval = self.handler.dispatcher(args, lineno)
499 def registerSection(self, obj):
500 """Given an instance of a Section subclass, register the new section
501 with the parser. Calling this method means the parser will
502 recognize your new section and dispatch into the given object to
505 if not obj.sectionOpen:
506 raise TypeError, "no sectionOpen given for section %s" % obj
508 if not obj.sectionOpen.startswith("%"):
509 raise TypeError, "section %s tag does not start with a %%" % obj.sectionOpen
511 self._sections[obj.sectionOpen] = obj
513 def _finalize(self, obj):
514 """Called at the close of a kickstart section to take any required
515 actions. Internally, this is used to add scripts once we have the
519 self._state = STATE_COMMANDS
521 def _handleSpecialComments(self, line):
522 """Kickstart recognizes a couple special comments."""
523 if self._state != STATE_COMMANDS:
526 # Save the platform for s-c-kickstart.
527 if line[:10] == "#platform=":
528 self.handler.platform = self._line[11:]
530 def _readSection(self, lineIter, lineno):
531 obj = self._sections[self._state]
535 line = lineIter.next()
537 # This section ends at the end of the file.
538 if self.version >= version.F8:
539 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Section does not end with %%end."))
542 except StopIteration:
547 # Throw away blank lines and comments, unless the section wants all
549 if self._isBlankOrComment(line) and not obj.allLines:
552 if line.startswith("%"):
553 args = shlex.split(line)
555 if args and args[0] == "%end":
556 # This is a properly terminated section.
559 elif args and args[0] == "%ksappend":
561 elif args and (self._validState(args[0]) or args[0] in ["%include", "%ksappend"]):
562 # This is an unterminated section.
563 if self.version >= version.F8:
564 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Section does not end with %%end."))
566 # Finish up. We do not process the header here because
567 # kicking back out to STATE_COMMANDS will ensure that happens.
573 # This is just a line within a section. Pass it off to whatever
574 # section handles it.
579 def _validState(self, st):
580 """Is the given section tag one that has been registered with the parser?"""
581 return st in self._sections.keys()
583 def _tryFunc(self, fn):
584 """Call the provided function (which doesn't take any arguments) and
585 do the appropriate error handling. If errorsAreFatal is False, this
586 function will just print the exception and keep going.
590 except Exception, msg:
591 if self.errorsAreFatal:
596 def _isBlankOrComment(self, line):
597 return line.isspace() or line == "" or line.lstrip()[0] == '#'
599 def _stateMachine(self, lineIter):
600 # For error reporting.
604 # Get the next line out of the file, quitting if this is the last line.
606 self._line = lineIter.next()
609 except StopIteration:
614 # Eliminate blank lines, whitespace-only lines, and comments.
615 if self._isBlankOrComment(self._line):
616 self._handleSpecialComments(self._line)
619 # Remove any end-of-line comments.
620 sanitized = self._line.split("#")[0]
622 # Then split the line.
623 args = shlex.split(sanitized.rstrip())
625 if args[0] == "%include":
626 # This case comes up primarily in ksvalidator.
627 if not self.followIncludes:
630 if len(args) == 1 or not args[1]:
631 raise KickstartParseError, formatErrorMsg(lineno)
633 self._includeDepth += 1
636 self.readKickstart(args[1], reset=False)
637 except KickstartError:
638 # Handle the include file being provided over the
639 # network in a %pre script. This case comes up in the
640 # early parsing in anaconda.
641 if self.missingIncludeIsFatal:
644 self._includeDepth -= 1
647 # Now on to the main event.
648 if self._state == STATE_COMMANDS:
649 if args[0] == "%ksappend":
650 # This is handled by the preprocess* functions, so continue.
652 elif args[0][0] == '%':
653 # This is the beginning of a new section. Handle its header
656 if not self._validState(newSection):
657 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Unknown kickstart section: %s" % newSection))
659 self._state = newSection
660 obj = self._sections[self._state]
661 self._tryFunc(lambda: obj.handleHeader(lineno, args))
663 # This will handle all section processing, kicking us back
664 # out to STATE_COMMANDS at the end with the current line
665 # being the next section header, etc.
666 lineno = self._readSection(lineIter, lineno)
668 # This is a command in the command section. Dispatch to it.
669 self._tryFunc(lambda: self.handleCommand(lineno, args))
670 elif self._state == STATE_END:
673 def readKickstartFromString (self, s, reset=True):
674 """Process a kickstart file, provided as the string str."""
678 # Add a "" to the end of the list so the string reader acts like the
679 # file reader and we only get StopIteration when we're after the final
681 i = PutBackIterator(s.splitlines(True) + [""])
682 self._stateMachine (i)
684 def readKickstart(self, f, reset=True):
685 """Process a kickstart file, given by the filename f."""
689 # an %include might not specify a full path. if we don't try to figure
690 # out what the path should have been, then we're unable to find it
691 # requiring full path specification, though, sucks. so let's make
692 # the reading "smart" by keeping track of what the path is at each
694 if not os.path.exists(f):
695 if self.currentdir.has_key(self._includeDepth - 1):
696 if os.path.exists(os.path.join(self.currentdir[self._includeDepth - 1], f)):
697 f = os.path.join(self.currentdir[self._includeDepth - 1], f)
699 cd = os.path.dirname(f)
700 if not cd.startswith("/"):
701 cd = os.path.abspath(cd)
702 self.currentdir[self._includeDepth] = cd
706 except grabber.URLGrabError, e:
707 raise KickstartError, formatErrorMsg(0, msg=_("Unable to open input kickstart file: %s") % e.strerror)
709 self.readKickstartFromString(s, reset=False)
711 def setupSections(self):
712 """Install the sections all kickstart files support. You may override
713 this method in a subclass, but should avoid doing so unless you know
718 # Install the sections all kickstart files support.
719 self.registerSection(PreScriptSection(self.handler, dataObj=Script))
720 self.registerSection(PostScriptSection(self.handler, dataObj=Script))
721 self.registerSection(TracebackScriptSection(self.handler, dataObj=Script))
722 self.registerSection(RunScriptSection(self.handler, dataObj=Script))
723 self.registerSection(PackageSection(self.handler))
724 self.registerSection(TpkPackageSection(self.handler))