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.
34 from collections import Iterator
40 from optparse import *
41 from urlgrabber import urlread
42 import urlgrabber.grabber as grabber
45 from errors import KickstartError, KickstartParseError, KickstartValueError, formatErrorMsg
46 from ko import KickstartObject
47 from sections import *
51 _ = lambda x: gettext.ldgettext("pykickstart", x)
54 STATE_COMMANDS = "commands"
58 def _preprocessStateMachine (lineIter):
62 # Now open an output kickstart file that we are going to write to one
64 (outF, outName) = tempfile.mkstemp("-ks.cfg", "", "/tmp")
72 # At the end of the file?
80 if not ll.startswith("%ksappend"):
84 # Try to pull down the remote file.
86 ksurl = ll.split(' ')[1]
88 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Illegal url for %%ksappend: %s") % ll)
91 url = grabber.urlopen(ksurl)
92 except grabber.URLGrabError, e:
93 raise KickstartError, formatErrorMsg(lineno, msg=_("Unable to open %%ksappend file: %s") % e.strerror)
95 # Sanity check result. Sometimes FTP doesn't catch a file
99 raise KickstartError, formatErrorMsg(lineno, msg=_("Unable to open %%ksappend file"))
101 raise KickstartError, formatErrorMsg(lineno, msg=_("Unable to open %%ksappend file"))
103 # If that worked, write the remote file to the output kickstart
104 # file in one burst. Then close everything up to get ready to
105 # read ahead in the input file. This allows multiple %ksappend
108 os.write(outF, url.read())
111 # All done - close the temp file and return its location.
115 def preprocessFromString (s):
116 """Preprocess the kickstart file, provided as the string str. This
117 method is currently only useful for handling %ksappend lines,
118 which need to be fetched before the real kickstart parser can be
119 run. Returns the location of the complete kickstart file.
121 i = iter(s.splitlines(True) + [""])
122 rc = _preprocessStateMachine (i.next)
125 def preprocessKickstart (f):
126 """Preprocess the kickstart file, given by the filename file. This
127 method is currently only useful for handling %ksappend lines,
128 which need to be fetched before the real kickstart parser can be
129 run. Returns the location of the complete kickstart file.
133 except grabber.URLGrabError, e:
134 raise KickstartError, formatErrorMsg(0, msg=_("Unable to open input kickstart file: %s") % e.strerror)
136 rc = _preprocessStateMachine (iter(fh.readlines()))
140 class PutBackIterator(Iterator):
141 def __init__(self, iterable):
142 self._iterable = iter(iterable)
157 return self._iterable.next()
162 class Script(KickstartObject):
163 """A class representing a single kickstart script. If functionality beyond
164 just a data representation is needed (for example, a run method in
165 anaconda), Script may be subclassed. Although a run method is not
166 provided, most of the attributes of Script have to do with running the
167 script. Instances of Script are held in a list by the Version object.
169 def __init__(self, script, *args , **kwargs):
170 """Create a new Script instance. Instance attributes:
172 errorOnFail -- If execution of the script fails, should anaconda
173 stop, display an error, and then reboot without
174 running any other scripts?
175 inChroot -- Does the script execute in anaconda's chroot
177 interp -- The program that should be used to interpret this
179 lineno -- The line number this script starts on.
180 logfile -- Where all messages from the script should be logged.
181 script -- A string containing all the lines of the script.
182 type -- The type of the script, which can be KS_SCRIPT_* from
183 pykickstart.constants.
185 KickstartObject.__init__(self, *args, **kwargs)
186 self.script = "".join(script)
188 self.interp = kwargs.get("interp", "/bin/sh")
189 self.inChroot = kwargs.get("inChroot", False)
190 self.lineno = kwargs.get("lineno", None)
191 self.logfile = kwargs.get("logfile", None)
192 self.errorOnFail = kwargs.get("errorOnFail", False)
193 self.type = kwargs.get("type", constants.KS_SCRIPT_PRE)
196 """Return a string formatted for output to a kickstart file."""
199 if self.type == constants.KS_SCRIPT_PRE:
201 elif self.type == constants.KS_SCRIPT_POST:
203 elif self.type == constants.KS_SCRIPT_TRACEBACK:
204 retval += '\n%traceback'
205 elif self.type == constants.KS_SCRIPT_RUN:
206 retval += '\n%runscript'
208 if self.interp != "/bin/sh" and self.interp != "":
209 retval += " --interpreter=%s" % self.interp
210 if self.type == constants.KS_SCRIPT_POST and not self.inChroot:
211 retval += " --nochroot"
212 if self.logfile != None:
213 retval += " --logfile %s" % self.logfile
215 retval += " --erroronfail"
217 if self.script.endswith("\n"):
218 if ver >= version.F8:
219 return retval + "\n%s%%end\n" % self.script
221 return retval + "\n%s\n" % self.script
223 if ver >= version.F8:
224 return retval + "\n%s\n%%end\n" % self.script
226 return retval + "\n%s\n" % self.script
233 """A class representing a single group in the %packages section."""
234 def __init__(self, name="", include=constants.GROUP_DEFAULT):
235 """Create a new Group instance. Instance attributes:
237 name -- The group's identifier
238 include -- The level of how much of the group should be included.
239 Values can be GROUP_* from pykickstart.constants.
242 self.include = include
245 """Return a string formatted for output to a kickstart file."""
246 if self.include == constants.GROUP_REQUIRED:
247 return "@%s --nodefaults" % self.name
248 elif self.include == constants.GROUP_ALL:
249 return "@%s --optional" % self.name
251 return "@%s" % self.name
253 def __cmp__(self, other):
254 if self.name < other.name:
256 elif self.name > other.name:
260 class Packages(KickstartObject):
261 """A class representing the %packages section of the kickstart file."""
262 def __init__(self, *args, **kwargs):
263 """Create a new Packages instance. Instance attributes:
265 addBase -- Should the Base group be installed even if it is
267 default -- Should the default package set be selected?
268 excludedList -- A list of all the packages marked for exclusion in
269 the %packages section, without the leading minus
271 excludeDocs -- Should documentation in each package be excluded?
272 groupList -- A list of Group objects representing all the groups
273 specified in the %packages section. Names will be
274 stripped of the leading @ symbol.
275 excludedGroupList -- A list of Group objects representing all the
276 groups specified for removal in the %packages
277 section. Names will be stripped of the leading
279 handleMissing -- If unknown packages are specified in the %packages
280 section, should it be ignored or not? Values can
281 be KS_MISSING_* from pykickstart.constants.
282 packageList -- A list of all the packages specified in the
284 instLangs -- A list of languages to install.
286 KickstartObject.__init__(self, *args, **kwargs)
290 self.excludedList = []
291 self.excludedGroupList = []
292 self.excludeDocs = False
294 self.handleMissing = constants.KS_MISSING_PROMPT
295 self.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)
414 class KickstartParser:
415 """The kickstart file parser class as represented by a basic state
416 machine. To create a specialized parser, make a subclass and override
417 any of the methods you care about. Methods that don't need to do
418 anything may just pass. However, _stateMachine should never be
421 def __init__ (self, handler, followIncludes=True, errorsAreFatal=True,
422 missingIncludeIsFatal=True):
423 """Create a new KickstartParser instance. Instance attributes:
425 errorsAreFatal -- Should errors cause processing to halt, or
426 just print a message to the screen? This
427 is most useful for writing syntax checkers
428 that may want to continue after an error is
430 followIncludes -- If %include is seen, should the included
431 file be checked as well or skipped?
432 handler -- An instance of a BaseHandler subclass. If
433 None, the input file will still be parsed
434 but no data will be saved and no commands
436 missingIncludeIsFatal -- Should missing include files be fatal, even
437 if errorsAreFatal is False?
439 self.errorsAreFatal = errorsAreFatal
440 self.followIncludes = followIncludes
441 self.handler = handler
443 self.missingIncludeIsFatal = missingIncludeIsFatal
445 self._state = STATE_COMMANDS
446 self._includeDepth = 0
449 self.version = self.handler.version
458 """Reset the internal variables of the state machine for a new kickstart file."""
459 self._state = STATE_COMMANDS
460 self._includeDepth = 0
462 def getSection(self, s):
463 """Return a reference to the requested section (s must start with '%'s),
464 or raise KeyError if not found.
466 return self._sections[s]
468 def handleCommand (self, lineno, args):
469 """Given the list of command and arguments, call the Version's
470 dispatcher method to handle the command. Returns the command or
471 data object returned by the dispatcher. This method may be
472 overridden in a subclass if necessary.
475 self.handler.currentCmd = args[0]
476 self.handler.currentLine = self._line
477 retval = self.handler.dispatcher(args, lineno)
481 def registerSection(self, obj):
482 """Given an instance of a Section subclass, register the new section
483 with the parser. Calling this method means the parser will
484 recognize your new section and dispatch into the given object to
487 if not obj.sectionOpen:
488 raise TypeError, "no sectionOpen given for section %s" % obj
490 if not obj.sectionOpen.startswith("%"):
491 raise TypeError, "section %s tag does not start with a %%" % obj.sectionOpen
493 self._sections[obj.sectionOpen] = obj
495 def _finalize(self, obj):
496 """Called at the close of a kickstart section to take any required
497 actions. Internally, this is used to add scripts once we have the
501 self._state = STATE_COMMANDS
503 def _handleSpecialComments(self, line):
504 """Kickstart recognizes a couple special comments."""
505 if self._state != STATE_COMMANDS:
508 # Save the platform for s-c-kickstart.
509 if line[:10] == "#platform=":
510 self.handler.platform = self._line[11:]
512 def _readSection(self, lineIter, lineno):
513 obj = self._sections[self._state]
517 line = lineIter.next()
519 # This section ends at the end of the file.
520 if self.version >= version.F8:
521 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Section does not end with %%end."))
524 except StopIteration:
529 # Throw away blank lines and comments, unless the section wants all
531 if self._isBlankOrComment(line) and not obj.allLines:
534 if line.startswith("%"):
535 args = shlex.split(line)
537 if args and args[0] == "%end":
538 # This is a properly terminated section.
541 elif args and args[0] == "%ksappend":
543 elif args and (self._validState(args[0]) or args[0] in ["%include", "%ksappend"]):
544 # This is an unterminated section.
545 if self.version >= version.F8:
546 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Section does not end with %%end."))
548 # Finish up. We do not process the header here because
549 # kicking back out to STATE_COMMANDS will ensure that happens.
555 # This is just a line within a section. Pass it off to whatever
556 # section handles it.
561 def _validState(self, st):
562 """Is the given section tag one that has been registered with the parser?"""
563 return st in self._sections.keys()
565 def _tryFunc(self, fn):
566 """Call the provided function (which doesn't take any arguments) and
567 do the appropriate error handling. If errorsAreFatal is False, this
568 function will just print the exception and keep going.
572 except Exception, msg:
573 if self.errorsAreFatal:
578 def _isBlankOrComment(self, line):
579 return line.isspace() or line == "" or line.lstrip()[0] == '#'
581 def _stateMachine(self, lineIter):
582 # For error reporting.
586 # Get the next line out of the file, quitting if this is the last line.
588 self._line = lineIter.next()
591 except StopIteration:
596 # Eliminate blank lines, whitespace-only lines, and comments.
597 if self._isBlankOrComment(self._line):
598 self._handleSpecialComments(self._line)
601 # Remove any end-of-line comments.
602 sanitized = self._line.split("#")[0]
604 # Then split the line.
605 args = shlex.split(sanitized.rstrip())
607 if args[0] == "%include":
608 # This case comes up primarily in ksvalidator.
609 if not self.followIncludes:
612 if len(args) == 1 or not args[1]:
613 raise KickstartParseError, formatErrorMsg(lineno)
615 self._includeDepth += 1
618 self.readKickstart(args[1], reset=False)
619 except KickstartError:
620 # Handle the include file being provided over the
621 # network in a %pre script. This case comes up in the
622 # early parsing in anaconda.
623 if self.missingIncludeIsFatal:
626 self._includeDepth -= 1
629 # Now on to the main event.
630 if self._state == STATE_COMMANDS:
631 if args[0] == "%ksappend":
632 # This is handled by the preprocess* functions, so continue.
634 elif args[0][0] == '%':
635 # This is the beginning of a new section. Handle its header
638 if not self._validState(newSection):
639 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Unknown kickstart section: %s" % newSection))
641 self._state = newSection
642 obj = self._sections[self._state]
643 self._tryFunc(lambda: obj.handleHeader(lineno, args))
645 # This will handle all section processing, kicking us back
646 # out to STATE_COMMANDS at the end with the current line
647 # being the next section header, etc.
648 lineno = self._readSection(lineIter, lineno)
650 # This is a command in the command section. Dispatch to it.
651 self._tryFunc(lambda: self.handleCommand(lineno, args))
652 elif self._state == STATE_END:
655 def readKickstartFromString (self, s, reset=True):
656 """Process a kickstart file, provided as the string str."""
660 # Add a "" to the end of the list so the string reader acts like the
661 # file reader and we only get StopIteration when we're after the final
663 i = PutBackIterator(s.splitlines(True) + [""])
664 self._stateMachine (i)
666 def readKickstart(self, f, reset=True):
667 """Process a kickstart file, given by the filename f."""
671 # an %include might not specify a full path. if we don't try to figure
672 # out what the path should have been, then we're unable to find it
673 # requiring full path specification, though, sucks. so let's make
674 # the reading "smart" by keeping track of what the path is at each
676 if not os.path.exists(f):
677 if self.currentdir.has_key(self._includeDepth - 1):
678 if os.path.exists(os.path.join(self.currentdir[self._includeDepth - 1], f)):
679 f = os.path.join(self.currentdir[self._includeDepth - 1], f)
681 cd = os.path.dirname(f)
682 if not cd.startswith("/"):
683 cd = os.path.abspath(cd)
684 self.currentdir[self._includeDepth] = cd
688 except grabber.URLGrabError, e:
689 raise KickstartError, formatErrorMsg(0, msg=_("Unable to open input kickstart file: %s") % e.strerror)
691 self.readKickstartFromString(s, reset=False)
693 def setupSections(self):
694 """Install the sections all kickstart files support. You may override
695 this method in a subclass, but should avoid doing so unless you know
700 # Install the sections all kickstart files support.
701 self.registerSection(PreScriptSection(self.handler, dataObj=Script))
702 self.registerSection(PostScriptSection(self.handler, dataObj=Script))
703 self.registerSection(TracebackScriptSection(self.handler, dataObj=Script))
704 self.registerSection(RunScriptSection(self.handler, dataObj=Script))
705 self.registerSection(PackageSection(self.handler))