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'
206 elif self.type == constants.KS_SCRIPT_UMOUNT:
207 retval += '\n%post-umount'
209 if self.interp != "/bin/sh" and self.interp != "":
210 retval += " --interpreter=%s" % self.interp
211 if self.type == constants.KS_SCRIPT_POST and not self.inChroot:
212 retval += " --nochroot"
213 if self.logfile != None:
214 retval += " --logfile %s" % self.logfile
216 retval += " --erroronfail"
218 if self.script.endswith("\n"):
219 if ver >= version.F8:
220 return retval + "\n%s%%end\n" % self.script
222 return retval + "\n%s\n" % self.script
224 if ver >= version.F8:
225 return retval + "\n%s\n%%end\n" % self.script
227 return retval + "\n%s\n" % self.script
234 """A class representing a single group in the %packages section."""
235 def __init__(self, name="", include=constants.GROUP_DEFAULT):
236 """Create a new Group instance. Instance attributes:
238 name -- The group's identifier
239 include -- The level of how much of the group should be included.
240 Values can be GROUP_* from pykickstart.constants.
243 self.include = include
246 """Return a string formatted for output to a kickstart file."""
247 if self.include == constants.GROUP_REQUIRED:
248 return "@%s --nodefaults" % self.name
249 elif self.include == constants.GROUP_ALL:
250 return "@%s --optional" % self.name
252 return "@%s" % self.name
254 def __cmp__(self, other):
255 if self.name < other.name:
257 elif self.name > other.name:
261 class Packages(KickstartObject):
262 """A class representing the %packages section of the kickstart file."""
263 def __init__(self, *args, **kwargs):
264 """Create a new Packages instance. Instance attributes:
266 addBase -- Should the Base group be installed even if it is
268 default -- Should the default package set be selected?
269 excludedList -- A list of all the packages marked for exclusion in
270 the %packages section, without the leading minus
272 excludeDocs -- Should documentation in each package be excluded?
273 groupList -- A list of Group objects representing all the groups
274 specified in the %packages section. Names will be
275 stripped of the leading @ symbol.
276 excludedGroupList -- A list of Group objects representing all the
277 groups specified for removal in the %packages
278 section. Names will be stripped of the leading
280 handleMissing -- If unknown packages are specified in the %packages
281 section, should it be ignored or not? Values can
282 be KS_MISSING_* from pykickstart.constants.
283 packageList -- A list of all the packages specified in the
285 instLangs -- A list of languages to install.
287 KickstartObject.__init__(self, *args, **kwargs)
291 self.excludedList = []
292 self.excludedGroupList = []
293 self.excludeDocs = False
295 self.handleMissing = constants.KS_MISSING_PROMPT
296 self.packageList = []
297 self.tpk_packageList = []
298 self.instLangs = None
301 """Return a string formatted for output to a kickstart file."""
305 grps = self.groupList
308 pkgs += "%s\n" % grp.__str__()
315 grps = self.excludedGroupList
318 pkgs += "-%s\n" % grp.__str__()
320 p = self.excludedList
323 pkgs += "-%s\n" % pkg
328 retval = "\n%packages"
331 retval += " --default"
333 retval += " --excludedocs"
335 retval += " --nobase"
336 if self.handleMissing == constants.KS_MISSING_IGNORE:
337 retval += " --ignoremissing"
339 retval += " --instLangs=%s" % self.instLangs
341 if ver >= version.F8:
342 return retval + "\n" + pkgs + "\n%end\n"
344 return retval + "\n" + pkgs + "\n"
346 def _processGroup (self, line):
348 op.add_option("--nodefaults", action="store_true", default=False)
349 op.add_option("--optional", action="store_true", default=False)
351 (opts, extra) = op.parse_args(args=line.split())
353 if opts.nodefaults and opts.optional:
354 raise KickstartValueError, _("Group cannot specify both --nodefaults and --optional")
356 # If the group name has spaces in it, we have to put it back together
358 grp = " ".join(extra)
361 self.groupList.append(Group(name=grp, include=constants.GROUP_REQUIRED))
363 self.groupList.append(Group(name=grp, include=constants.GROUP_ALL))
365 self.groupList.append(Group(name=grp, include=constants.GROUP_DEFAULT))
367 def add (self, pkgList):
368 """Given a list of lines from the input file, strip off any leading
369 symbols and add the result to the appropriate list.
371 existingExcludedSet = set(self.excludedList)
372 existingPackageSet = set(self.packageList)
373 newExcludedSet = set()
374 newPackageSet = set()
376 excludedGroupList = []
379 stripped = pkg.strip()
381 if stripped[0] == "@":
382 self._processGroup(stripped[1:])
383 elif stripped[0] == "-":
384 if stripped[1] == "@":
385 excludedGroupList.append(Group(name=stripped[2:]))
387 newExcludedSet.add(stripped[1:])
389 newPackageSet.add(stripped)
391 # Groups have to be excluded in two different ways (note: can't use
392 # sets here because we have to store objects):
393 excludedGroupNames = map(lambda g: g.name, excludedGroupList)
395 # First, an excluded group may be cancelling out a previously given
396 # one. This is often the case when using %include. So there we should
397 # just remove the group from the list.
398 self.groupList = filter(lambda g: g.name not in excludedGroupNames, self.groupList)
400 # Second, the package list could have included globs which are not
401 # processed by pykickstart. In that case we need to preserve a list of
402 # excluded groups so whatever tool doing package/group installation can
403 # take appropriate action.
404 self.excludedGroupList.extend(excludedGroupList)
406 existingPackageSet = (existingPackageSet - newExcludedSet) | newPackageSet
407 existingExcludedSet = (existingExcludedSet - existingPackageSet) | newExcludedSet
409 self.packageList = list(existingPackageSet)
410 self.excludedList = list(existingExcludedSet)
412 class TpkPackages(KickstartObject):
413 """A class representing the %tpk_packages section of the kickstart file."""
414 def __init__(self, *args, **kwargs):
415 KickstartObject.__init__(self, *args, **kwargs)
416 self.tpk_packageList = []
419 retval = "\n%tpk_packages"
423 tpk_pkgs += "%s\n" % pkg
424 return retval + "\n" +tpk_pkgs
425 def add(self, tpkPackageList):
426 tpk_PackageSet = set(self.tpk_packageList)
427 for tpk_pkg in tpkPackageList:
428 stripped = tpk_pkg.strip()
429 tpk_PackageSet.add(stripped)
430 self.tpk_packageList = list(tpk_PackageSet)
434 class KickstartParser:
435 """The kickstart file parser class as represented by a basic state
436 machine. To create a specialized parser, make a subclass and override
437 any of the methods you care about. Methods that don't need to do
438 anything may just pass. However, _stateMachine should never be
441 def __init__ (self, handler, followIncludes=True, errorsAreFatal=True,
442 missingIncludeIsFatal=True):
443 """Create a new KickstartParser instance. Instance attributes:
445 errorsAreFatal -- Should errors cause processing to halt, or
446 just print a message to the screen? This
447 is most useful for writing syntax checkers
448 that may want to continue after an error is
450 followIncludes -- If %include is seen, should the included
451 file be checked as well or skipped?
452 handler -- An instance of a BaseHandler subclass. If
453 None, the input file will still be parsed
454 but no data will be saved and no commands
456 missingIncludeIsFatal -- Should missing include files be fatal, even
457 if errorsAreFatal is False?
459 self.errorsAreFatal = errorsAreFatal
460 self.followIncludes = followIncludes
461 self.handler = handler
463 self.missingIncludeIsFatal = missingIncludeIsFatal
465 self._state = STATE_COMMANDS
466 self._includeDepth = 0
469 self.version = self.handler.version
478 """Reset the internal variables of the state machine for a new kickstart file."""
479 self._state = STATE_COMMANDS
480 self._includeDepth = 0
482 def getSection(self, s):
483 """Return a reference to the requested section (s must start with '%'s),
484 or raise KeyError if not found.
486 return self._sections[s]
488 def handleCommand (self, lineno, args):
489 """Given the list of command and arguments, call the Version's
490 dispatcher method to handle the command. Returns the command or
491 data object returned by the dispatcher. This method may be
492 overridden in a subclass if necessary.
495 self.handler.currentCmd = args[0]
496 self.handler.currentLine = self._line
497 retval = self.handler.dispatcher(args, lineno)
501 def registerSection(self, obj):
502 """Given an instance of a Section subclass, register the new section
503 with the parser. Calling this method means the parser will
504 recognize your new section and dispatch into the given object to
507 if not obj.sectionOpen:
508 raise TypeError, "no sectionOpen given for section %s" % obj
510 if not obj.sectionOpen.startswith("%"):
511 raise TypeError, "section %s tag does not start with a %%" % obj.sectionOpen
513 self._sections[obj.sectionOpen] = obj
515 def _finalize(self, obj):
516 """Called at the close of a kickstart section to take any required
517 actions. Internally, this is used to add scripts once we have the
521 self._state = STATE_COMMANDS
523 def _handleSpecialComments(self, line):
524 """Kickstart recognizes a couple special comments."""
525 if self._state != STATE_COMMANDS:
528 # Save the platform for s-c-kickstart.
529 if line[:10] == "#platform=":
530 self.handler.platform = self._line[11:]
532 def _readSection(self, lineIter, lineno):
533 obj = self._sections[self._state]
537 line = lineIter.next()
539 # This section ends at the end of the file.
540 if self.version >= version.F8:
541 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Section does not end with %%end."))
544 except StopIteration:
549 # Throw away blank lines and comments, unless the section wants all
551 if self._isBlankOrComment(line) and not obj.allLines:
554 if line.startswith("%"):
555 args = shlex.split(line)
557 if args and args[0] == "%end":
558 # This is a properly terminated section.
561 elif args and args[0] == "%ksappend":
563 elif args and (self._validState(args[0]) or args[0] in ["%include", "%ksappend"]):
564 # This is an unterminated section.
565 if self.version >= version.F8:
566 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Section does not end with %%end."))
568 # Finish up. We do not process the header here because
569 # kicking back out to STATE_COMMANDS will ensure that happens.
575 # This is just a line within a section. Pass it off to whatever
576 # section handles it.
581 def _validState(self, st):
582 """Is the given section tag one that has been registered with the parser?"""
583 return st in self._sections.keys()
585 def _tryFunc(self, fn):
586 """Call the provided function (which doesn't take any arguments) and
587 do the appropriate error handling. If errorsAreFatal is False, this
588 function will just print the exception and keep going.
592 except Exception, msg:
593 if self.errorsAreFatal:
598 def _isBlankOrComment(self, line):
599 return line.isspace() or line == "" or line.lstrip()[0] == '#'
601 def _stateMachine(self, lineIter):
602 # For error reporting.
606 # Get the next line out of the file, quitting if this is the last line.
608 self._line = lineIter.next()
611 except StopIteration:
616 # Eliminate blank lines, whitespace-only lines, and comments.
617 if self._isBlankOrComment(self._line):
618 self._handleSpecialComments(self._line)
621 # Remove any end-of-line comments.
622 sanitized = self._line.split("#")[0]
624 # Then split the line.
625 args = shlex.split(sanitized.rstrip())
627 if args[0] == "%include":
628 # This case comes up primarily in ksvalidator.
629 if not self.followIncludes:
632 if len(args) == 1 or not args[1]:
633 raise KickstartParseError, formatErrorMsg(lineno)
635 self._includeDepth += 1
638 self.readKickstart(args[1], reset=False)
639 except KickstartError:
640 # Handle the include file being provided over the
641 # network in a %pre script. This case comes up in the
642 # early parsing in anaconda.
643 if self.missingIncludeIsFatal:
646 self._includeDepth -= 1
649 # Now on to the main event.
650 if self._state == STATE_COMMANDS:
651 if args[0] == "%ksappend":
652 # This is handled by the preprocess* functions, so continue.
654 elif args[0][0] == '%':
655 # This is the beginning of a new section. Handle its header
658 if not self._validState(newSection):
659 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Unknown kickstart section: %s" % newSection))
661 self._state = newSection
662 obj = self._sections[self._state]
663 self._tryFunc(lambda: obj.handleHeader(lineno, args))
665 # This will handle all section processing, kicking us back
666 # out to STATE_COMMANDS at the end with the current line
667 # being the next section header, etc.
668 lineno = self._readSection(lineIter, lineno)
670 # This is a command in the command section. Dispatch to it.
671 self._tryFunc(lambda: self.handleCommand(lineno, args))
672 elif self._state == STATE_END:
675 def readKickstartFromString (self, s, reset=True):
676 """Process a kickstart file, provided as the string str."""
680 # Add a "" to the end of the list so the string reader acts like the
681 # file reader and we only get StopIteration when we're after the final
683 i = PutBackIterator(s.splitlines(True) + [""])
684 self._stateMachine (i)
686 def readKickstart(self, f, reset=True):
687 """Process a kickstart file, given by the filename f."""
691 # an %include might not specify a full path. if we don't try to figure
692 # out what the path should have been, then we're unable to find it
693 # requiring full path specification, though, sucks. so let's make
694 # the reading "smart" by keeping track of what the path is at each
696 if not os.path.exists(f):
697 if self.currentdir.has_key(self._includeDepth - 1):
698 if os.path.exists(os.path.join(self.currentdir[self._includeDepth - 1], f)):
699 f = os.path.join(self.currentdir[self._includeDepth - 1], f)
701 cd = os.path.dirname(f)
702 if not cd.startswith("/"):
703 cd = os.path.abspath(cd)
704 self.currentdir[self._includeDepth] = cd
708 except grabber.URLGrabError, e:
709 raise KickstartError, formatErrorMsg(0, msg=_("Unable to open input kickstart file: %s") % e.strerror)
711 self.readKickstartFromString(s, reset=False)
713 def setupSections(self):
714 """Install the sections all kickstart files support. You may override
715 this method in a subclass, but should avoid doing so unless you know
720 # Install the sections all kickstart files support.
721 self.registerSection(PreScriptSection(self.handler, dataObj=Script))
722 self.registerSection(PostScriptSection(self.handler, dataObj=Script))
723 self.registerSection(TracebackScriptSection(self.handler, dataObj=Script))
724 self.registerSection(RunScriptSection(self.handler, dataObj=Script))
725 self.registerSection(PostUmountScriptSection(self.handler, dataObj=Script))
726 self.registerSection(PackageSection(self.handler))
727 self.registerSection(TpkPackageSection(self.handler))