Allow recommended flag for non-prexisting logical volumes (#1149718)
[pykickstart.git] / pykickstart / parser.py
blob71c04443e93e7f84c6586749ae0aef2404c29e4a
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.
22 """
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.
32 """
34 from collections import Iterator
35 import os
36 import shlex
37 import tempfile
38 from optparse import OptionParser
39 from urlgrabber import urlread
40 import urlgrabber.grabber as grabber
42 from pykickstart import constants, version
43 from pykickstart.errors import KickstartError, KickstartParseError, KickstartValueError, formatErrorMsg
44 from pykickstart.ko import KickstartObject
45 from pykickstart.sections import PackageSection, PreScriptSection, PostScriptSection, TracebackScriptSection
47 import gettext
48 _ = lambda x: gettext.ldgettext("pykickstart", x)
50 STATE_END = "end"
51 STATE_COMMANDS = "commands"
53 ver = version.DEVEL
55 def _preprocessStateMachine (lineIter):
56 l = None
57 lineno = 0
59 # Now open an output kickstart file that we are going to write to one
60 # line at a time.
61 (outF, outName) = tempfile.mkstemp("-ks.cfg", "", "/tmp")
63 while True:
64 try:
65 l = lineIter.next()
66 except StopIteration:
67 break
69 # At the end of the file?
70 if l == "":
71 break
73 lineno += 1
74 url = None
76 ll = l.strip()
77 if not ll.startswith("%ksappend"):
78 os.write(outF, l)
79 continue
81 # Try to pull down the remote file.
82 try:
83 ksurl = ll.split(' ')[1]
84 except:
85 raise KickstartParseError(formatErrorMsg(lineno, msg=_("Illegal url for %%ksappend: %s") % ll))
87 try:
88 url = grabber.urlopen(ksurl)
89 except grabber.URLGrabError, e:
90 raise KickstartError(formatErrorMsg(lineno, msg=_("Unable to open %%ksappend file: %s") % e.strerror))
91 else:
92 # Sanity check result. Sometimes FTP doesn't catch a file
93 # is missing.
94 try:
95 if url.size < 1:
96 raise KickstartError(formatErrorMsg(lineno, msg=_("Unable to open %%ksappend file")))
97 except:
98 raise KickstartError(formatErrorMsg(lineno, msg=_("Unable to open %%ksappend file")))
100 # If that worked, write the remote file to the output kickstart
101 # file in one burst. Then close everything up to get ready to
102 # read ahead in the input file. This allows multiple %ksappend
103 # lines to exist.
104 if url is not None:
105 os.write(outF, url.read())
106 url.close()
108 # All done - close the temp file and return its location.
109 os.close(outF)
110 return outName
112 def preprocessFromString (s):
113 """Preprocess the kickstart file, provided as the string str. This
114 method is currently only useful for handling %ksappend lines,
115 which need to be fetched before the real kickstart parser can be
116 run. Returns the location of the complete kickstart file.
118 i = iter(s.splitlines(True) + [""])
119 rc = _preprocessStateMachine (i.next)
120 return rc
122 def preprocessKickstart (f):
123 """Preprocess the kickstart file, given by the filename file. This
124 method is currently only useful for handling %ksappend lines,
125 which need to be fetched before the real kickstart parser can be
126 run. Returns the location of the complete kickstart file.
128 try:
129 fh = grabber.urlopen(f)
130 except grabber.URLGrabError, e:
131 raise KickstartError(formatErrorMsg(0, msg=_("Unable to open input kickstart file: %s") % e.strerror))
133 rc = _preprocessStateMachine (iter(fh.readlines()))
134 fh.close()
135 return rc
137 class PutBackIterator(Iterator):
138 def __init__(self, iterable):
139 self._iterable = iter(iterable)
140 self._buf = None
142 def __iter__(self):
143 return self
145 def put(self, s):
146 self._buf = s
148 def next(self):
149 if self._buf:
150 retval = self._buf
151 self._buf = None
152 return retval
153 else:
154 return self._iterable.next()
157 ### SCRIPT HANDLING
159 class Script(KickstartObject):
160 """A class representing a single kickstart script. If functionality beyond
161 just a data representation is needed (for example, a run method in
162 anaconda), Script may be subclassed. Although a run method is not
163 provided, most of the attributes of Script have to do with running the
164 script. Instances of Script are held in a list by the Version object.
166 def __init__(self, script, *args , **kwargs):
167 """Create a new Script instance. Instance attributes:
169 errorOnFail -- If execution of the script fails, should anaconda
170 stop, display an error, and then reboot without
171 running any other scripts?
172 inChroot -- Does the script execute in anaconda's chroot
173 environment or not?
174 interp -- The program that should be used to interpret this
175 script.
176 lineno -- The line number this script starts on.
177 logfile -- Where all messages from the script should be logged.
178 script -- A string containing all the lines of the script.
179 type -- The type of the script, which can be KS_SCRIPT_* from
180 pykickstart.constants.
182 KickstartObject.__init__(self, *args, **kwargs)
183 self.script = "".join(script)
185 self.interp = kwargs.get("interp", "/bin/sh")
186 self.inChroot = kwargs.get("inChroot", False)
187 self.lineno = kwargs.get("lineno", None)
188 self.logfile = kwargs.get("logfile", None)
189 self.errorOnFail = kwargs.get("errorOnFail", False)
190 self.type = kwargs.get("type", constants.KS_SCRIPT_PRE)
192 def __str__(self):
193 """Return a string formatted for output to a kickstart file."""
194 retval = ""
196 if self.type == constants.KS_SCRIPT_PRE:
197 retval += '\n%pre'
198 elif self.type == constants.KS_SCRIPT_POST:
199 retval += '\n%post'
200 elif self.type == constants.KS_SCRIPT_TRACEBACK:
201 retval += '\n%traceback'
203 if self.interp != "/bin/sh" and self.interp != "":
204 retval += " --interpreter=%s" % self.interp
205 if self.type == constants.KS_SCRIPT_POST and not self.inChroot:
206 retval += " --nochroot"
207 if self.logfile != None:
208 retval += " --logfile %s" % self.logfile
209 if self.errorOnFail:
210 retval += " --erroronfail"
212 if self.script.endswith("\n"):
213 if ver >= version.F8:
214 return retval + "\n%s%%end\n" % self.script
215 else:
216 return retval + "\n%s\n" % self.script
217 else:
218 if ver >= version.F8:
219 return retval + "\n%s\n%%end\n" % self.script
220 else:
221 return retval + "\n%s\n" % self.script
225 ## PACKAGE HANDLING
227 class Group:
228 """A class representing a single group in the %packages section."""
229 def __init__(self, name="", include=constants.GROUP_DEFAULT):
230 """Create a new Group instance. Instance attributes:
232 name -- The group's identifier
233 include -- The level of how much of the group should be included.
234 Values can be GROUP_* from pykickstart.constants.
236 self.name = name
237 self.include = include
239 def __str__(self):
240 """Return a string formatted for output to a kickstart file."""
241 if self.include == constants.GROUP_REQUIRED:
242 return "@%s --nodefaults" % self.name
243 elif self.include == constants.GROUP_ALL:
244 return "@%s --optional" % self.name
245 else:
246 return "@%s" % self.name
248 def __cmp__(self, other):
249 if self.name < other.name:
250 return -1
251 elif self.name > other.name:
252 return 1
253 return 0
255 class Packages(KickstartObject):
256 """A class representing the %packages section of the kickstart file."""
257 def __init__(self, *args, **kwargs):
258 """Create a new Packages instance. Instance attributes:
260 addBase -- Should the Base group be installed even if it is
261 not specified?
262 nocore -- Should the Core group be skipped? This results in
263 a %packages section that basically only installs the
264 packages you list, and may not be a usable system.
265 default -- Should the default package set be selected?
266 environment -- What base environment should be selected? Only one
267 may be chosen at a time.
268 excludedList -- A list of all the packages marked for exclusion in
269 the %packages section, without the leading minus
270 symbol.
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
278 -@ symbols.
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
283 %packages section.
284 instLangs -- A list of languages to install.
285 multiLib -- Whether to use yum's "all" multilib policy.
286 seen -- If %packages was ever used in the kickstart file,
287 this attribute will be set to True.
290 KickstartObject.__init__(self, *args, **kwargs)
292 self.addBase = True
293 self.nocore = False
294 self.default = False
295 self.environment = None
296 self.excludedList = []
297 self.excludedGroupList = []
298 self.excludeDocs = False
299 self.groupList = []
300 self.handleMissing = constants.KS_MISSING_PROMPT
301 self.packageList = []
302 self.instLangs = None
303 self.multiLib = False
304 self.seen = False
306 def __str__(self):
307 """Return a string formatted for output to a kickstart file."""
308 pkgs = ""
310 if not self.default:
311 if self.environment:
312 pkgs += "@^%s\n" % self.environment
314 grps = self.groupList
315 grps.sort()
316 for grp in grps:
317 pkgs += "%s\n" % grp.__str__()
319 p = self.packageList
320 p.sort()
321 for pkg in p:
322 pkgs += "%s\n" % pkg
324 grps = self.excludedGroupList
325 grps.sort()
326 for grp in grps:
327 pkgs += "-%s\n" % grp.__str__()
329 p = self.excludedList
330 p.sort()
331 for pkg in p:
332 pkgs += "-%s\n" % pkg
334 if pkgs == "":
335 return ""
337 retval = "\n%packages"
339 if self.default:
340 retval += " --default"
341 if self.excludeDocs:
342 retval += " --excludedocs"
343 if not self.addBase:
344 retval += " --nobase"
345 if self.nocore:
346 retval += " --nocore"
347 if self.handleMissing == constants.KS_MISSING_IGNORE:
348 retval += " --ignoremissing"
349 if self.instLangs:
350 retval += " --instLangs=%s" % self.instLangs
351 if self.multiLib:
352 retval += " --multilib"
354 if ver >= version.F8:
355 return retval + "\n" + pkgs + "\n%end\n"
356 else:
357 return retval + "\n" + pkgs + "\n"
359 def _processGroup (self, line):
360 op = OptionParser()
361 op.add_option("--nodefaults", action="store_true", default=False)
362 op.add_option("--optional", action="store_true", default=False)
364 (opts, extra) = op.parse_args(args=line.split())
366 if opts.nodefaults and opts.optional:
367 raise KickstartValueError(_("Group cannot specify both --nodefaults and --optional"))
369 # If the group name has spaces in it, we have to put it back together
370 # now.
371 grp = " ".join(extra)
373 if opts.nodefaults:
374 self.groupList.append(Group(name=grp, include=constants.GROUP_REQUIRED))
375 elif opts.optional:
376 self.groupList.append(Group(name=grp, include=constants.GROUP_ALL))
377 else:
378 self.groupList.append(Group(name=grp, include=constants.GROUP_DEFAULT))
380 def add (self, pkgList):
381 """Given a list of lines from the input file, strip off any leading
382 symbols and add the result to the appropriate list.
384 existingExcludedSet = set(self.excludedList)
385 existingPackageSet = set(self.packageList)
386 newExcludedSet = set()
387 newPackageSet = set()
389 excludedGroupList = []
391 for pkg in pkgList:
392 stripped = pkg.strip()
394 if stripped[0:2] == "@^":
395 self.environment = stripped[2:]
396 elif stripped[0] == "@":
397 self._processGroup(stripped[1:])
398 elif stripped[0] == "-":
399 if stripped[1:3] == "@^" and self.environment == stripped[3:]:
400 self.environment = None
401 elif stripped[1] == "@":
402 excludedGroupList.append(Group(name=stripped[2:]))
403 else:
404 newExcludedSet.add(stripped[1:])
405 else:
406 newPackageSet.add(stripped)
408 # Groups have to be excluded in two different ways (note: can't use
409 # sets here because we have to store objects):
410 excludedGroupNames = [g.name for g in excludedGroupList]
412 # First, an excluded group may be cancelling out a previously given
413 # one. This is often the case when using %include. So there we should
414 # just remove the group from the list.
415 self.groupList = [g for g in self.groupList if g.name not in excludedGroupNames]
417 # Second, the package list could have included globs which are not
418 # processed by pykickstart. In that case we need to preserve a list of
419 # excluded groups so whatever tool doing package/group installation can
420 # take appropriate action.
421 self.excludedGroupList.extend(excludedGroupList)
423 existingPackageSet = (existingPackageSet - newExcludedSet) | newPackageSet
424 existingExcludedSet = (existingExcludedSet - existingPackageSet) | newExcludedSet
426 self.packageList = list(existingPackageSet)
427 self.excludedList = list(existingExcludedSet)
431 ### PARSER
433 class KickstartParser:
434 """The kickstart file parser class as represented by a basic state
435 machine. To create a specialized parser, make a subclass and override
436 any of the methods you care about. Methods that don't need to do
437 anything may just pass. However, _stateMachine should never be
438 overridden.
440 def __init__ (self, handler, followIncludes=True, errorsAreFatal=True,
441 missingIncludeIsFatal=True):
442 """Create a new KickstartParser instance. Instance attributes:
444 errorsAreFatal -- Should errors cause processing to halt, or
445 just print a message to the screen? This
446 is most useful for writing syntax checkers
447 that may want to continue after an error is
448 encountered.
449 followIncludes -- If %include is seen, should the included
450 file be checked as well or skipped?
451 handler -- An instance of a BaseHandler subclass. If
452 None, the input file will still be parsed
453 but no data will be saved and no commands
454 will be executed.
455 missingIncludeIsFatal -- Should missing include files be fatal, even
456 if errorsAreFatal is False?
458 self.errorsAreFatal = errorsAreFatal
459 self.followIncludes = followIncludes
460 self.handler = handler
461 self.currentdir = {}
462 self.missingIncludeIsFatal = missingIncludeIsFatal
464 self._state = STATE_COMMANDS
465 self._includeDepth = 0
466 self._line = ""
468 self.version = self.handler.version
470 global ver
471 ver = self.version
473 self._sections = {}
474 self.setupSections()
476 def _reset(self):
477 """Reset the internal variables of the state machine for a new kickstart file."""
478 self._state = STATE_COMMANDS
479 self._includeDepth = 0
481 def getSection(self, s):
482 """Return a reference to the requested section (s must start with '%'s),
483 or raise KeyError if not found.
485 return self._sections[s]
487 def handleCommand (self, lineno, args):
488 """Given the list of command and arguments, call the Version's
489 dispatcher method to handle the command. Returns the command or
490 data object returned by the dispatcher. This method may be
491 overridden in a subclass if necessary.
493 if self.handler:
494 self.handler.currentCmd = args[0]
495 self.handler.currentLine = self._line
496 retval = self.handler.dispatcher(args, lineno)
498 return retval
500 def registerSection(self, obj):
501 """Given an instance of a Section subclass, register the new section
502 with the parser. Calling this method means the parser will
503 recognize your new section and dispatch into the given object to
504 handle it.
506 if not obj.sectionOpen:
507 raise TypeError("no sectionOpen given for section %s" % obj)
509 if not obj.sectionOpen.startswith("%"):
510 raise TypeError("section %s tag does not start with a %%" % obj.sectionOpen)
512 self._sections[obj.sectionOpen] = obj
514 def _finalize(self, obj):
515 """Called at the close of a kickstart section to take any required
516 actions. Internally, this is used to add scripts once we have the
517 whole body read.
519 obj.finalize()
520 self._state = STATE_COMMANDS
522 def _handleSpecialComments(self, line):
523 """Kickstart recognizes a couple special comments."""
524 if self._state != STATE_COMMANDS:
525 return
527 # Save the platform for s-c-kickstart.
528 if line[:10] == "#platform=":
529 self.handler.platform = self._line[11:]
531 def _readSection(self, lineIter, lineno):
532 obj = self._sections[self._state]
534 while True:
535 try:
536 line = lineIter.next()
537 if line == "" and self._includeDepth == 0:
538 # This section ends at the end of the file.
539 if self.version >= version.F8:
540 raise KickstartParseError(formatErrorMsg(lineno, msg=_("Section %s does not end with %%end.") % obj.sectionOpen))
542 self._finalize(obj)
543 except StopIteration:
544 break
546 lineno += 1
548 # Throw away blank lines and comments, unless the section wants all
549 # lines.
550 if self._isBlankOrComment(line) and not obj.allLines:
551 continue
553 if line.startswith("%"):
554 # If we're in a script, the line may begin with "%something"
555 # that's not the start of any section we recognize, but still
556 # valid for that script. So, don't do the split below unless
557 # we're sure.
558 possibleSectionStart = line.split()[0]
559 if not self._validState(possibleSectionStart) \
560 and possibleSectionStart not in ("%end", "%include"):
561 obj.handleLine(line)
562 continue
564 args = shlex.split(line)
566 if args and args[0] == "%end":
567 # This is a properly terminated section.
568 self._finalize(obj)
569 break
570 elif args and args[0] == "%include":
571 if len(args) == 1 or not args[1]:
572 raise KickstartParseError(formatErrorMsg(lineno))
574 self._handleInclude(args[1])
575 continue
576 elif args and args[0] == "%ksappend":
577 continue
578 elif args and self._validState(args[0]):
579 # This is an unterminated section.
580 if self.version >= version.F8:
581 raise KickstartParseError(formatErrorMsg(lineno, msg=_("Section %s does not end with %%end.") % obj.sectionOpen))
583 # Finish up. We do not process the header here because
584 # kicking back out to STATE_COMMANDS will ensure that happens.
585 lineIter.put(line)
586 lineno -= 1
587 self._finalize(obj)
588 break
589 else:
590 # This is just a line within a section. Pass it off to whatever
591 # section handles it.
592 obj.handleLine(line)
594 return lineno
596 def _validState(self, st):
597 """Is the given section tag one that has been registered with the parser?"""
598 return st in list(self._sections.keys())
600 def _tryFunc(self, fn):
601 """Call the provided function (which doesn't take any arguments) and
602 do the appropriate error handling. If errorsAreFatal is False, this
603 function will just print the exception and keep going.
605 try:
606 fn()
607 except Exception, msg:
608 if self.errorsAreFatal:
609 raise
610 else:
611 print(msg)
613 def _isBlankOrComment(self, line):
614 return line.isspace() or line == "" or line.lstrip()[0] == '#'
616 def _handleInclude(self, f):
617 # This case comes up primarily in ksvalidator.
618 if not self.followIncludes:
619 return
621 self._includeDepth += 1
623 try:
624 self.readKickstart(f, reset=False)
625 except KickstartError:
626 # Handle the include file being provided over the
627 # network in a %pre script. This case comes up in the
628 # early parsing in anaconda.
629 if self.missingIncludeIsFatal:
630 raise
632 self._includeDepth -= 1
634 def _stateMachine(self, lineIter):
635 # For error reporting.
636 lineno = 0
638 while True:
639 # Get the next line out of the file, quitting if this is the last line.
640 try:
641 self._line = lineIter.next()
642 if self._line == "":
643 break
644 except StopIteration:
645 break
647 lineno += 1
649 # Eliminate blank lines, whitespace-only lines, and comments.
650 if self._isBlankOrComment(self._line):
651 self._handleSpecialComments(self._line)
652 continue
654 # Split the line, discarding comments.
655 args = shlex.split(self._line, comments=True)
657 if args[0] == "%include":
658 if len(args) == 1 or not args[1]:
659 raise KickstartParseError(formatErrorMsg(lineno))
661 self._handleInclude(args[1])
662 continue
664 # Now on to the main event.
665 if self._state == STATE_COMMANDS:
666 if args[0] == "%ksappend":
667 # This is handled by the preprocess* functions, so continue.
668 continue
669 elif args[0][0] == '%':
670 # This is the beginning of a new section. Handle its header
671 # here.
672 newSection = args[0]
673 if not self._validState(newSection):
674 raise KickstartParseError(formatErrorMsg(lineno, msg=_("Unknown kickstart section: %s" % newSection)))
676 self._state = newSection
677 obj = self._sections[self._state]
678 self._tryFunc(lambda: obj.handleHeader(lineno, args))
680 # This will handle all section processing, kicking us back
681 # out to STATE_COMMANDS at the end with the current line
682 # being the next section header, etc.
683 lineno = self._readSection(lineIter, lineno)
684 else:
685 # This is a command in the command section. Dispatch to it.
686 self._tryFunc(lambda: self.handleCommand(lineno, args))
687 elif self._state == STATE_END:
688 break
689 elif self._includeDepth > 0:
690 lineIter.put(self._line)
691 lineno -= 1
692 lineno = self._readSection(lineIter, lineno)
694 def readKickstartFromString (self, s, reset=True):
695 """Process a kickstart file, provided as the string str."""
696 if reset:
697 self._reset()
699 # Add a "" to the end of the list so the string reader acts like the
700 # file reader and we only get StopIteration when we're after the final
701 # line of input.
702 i = PutBackIterator(s.splitlines(True) + [""])
703 self._stateMachine (i)
705 def readKickstart(self, f, reset=True):
706 """Process a kickstart file, given by the filename f."""
707 if reset:
708 self._reset()
710 # an %include might not specify a full path. if we don't try to figure
711 # out what the path should have been, then we're unable to find it
712 # requiring full path specification, though, sucks. so let's make
713 # the reading "smart" by keeping track of what the path is at each
714 # include depth.
715 if not os.path.exists(f):
716 if self._includeDepth - 1 in self.currentdir:
717 if os.path.exists(os.path.join(self.currentdir[self._includeDepth - 1], f)):
718 f = os.path.join(self.currentdir[self._includeDepth - 1], f)
720 cd = os.path.dirname(f)
721 if not cd.startswith("/"):
722 cd = os.path.abspath(cd)
723 self.currentdir[self._includeDepth] = cd
725 try:
726 s = urlread(f)
727 except grabber.URLGrabError, e:
728 raise KickstartError(formatErrorMsg(0, msg=_("Unable to open input kickstart file: %s") % e.strerror))
730 self.readKickstartFromString(s, reset=False)
732 def setupSections(self):
733 """Install the sections all kickstart files support. You may override
734 this method in a subclass, but should avoid doing so unless you know
735 what you're doing.
737 self._sections = {}
739 # Install the sections all kickstart files support.
740 self.registerSection(PreScriptSection(self.handler, dataObj=Script))
741 self.registerSection(PostScriptSection(self.handler, dataObj=Script))
742 self.registerSection(TracebackScriptSection(self.handler, dataObj=Script))
743 self.registerSection(PackageSection(self.handler))