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
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
48 _
= lambda x
: gettext
.ldgettext("pykickstart", x
)
51 STATE_COMMANDS
= "commands"
55 def _preprocessStateMachine (lineIter
):
59 # Now open an output kickstart file that we are going to write to one
61 (outF
, outName
) = tempfile
.mkstemp("-ks.cfg", "", "/tmp")
69 # At the end of the file?
77 if not ll
.startswith("%ksappend"):
81 # Try to pull down the remote file.
83 ksurl
= ll
.split(' ')[1]
85 raise KickstartParseError(formatErrorMsg(lineno
, msg
=_("Illegal url for %%ksappend: %s") % ll
))
88 url
= grabber
.urlopen(ksurl
)
89 except grabber
.URLGrabError
, e
:
90 raise KickstartError(formatErrorMsg(lineno
, msg
=_("Unable to open %%ksappend file: %s") % e
.strerror
))
92 # Sanity check result. Sometimes FTP doesn't catch a file
96 raise KickstartError(formatErrorMsg(lineno
, msg
=_("Unable to open %%ksappend file")))
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
105 os
.write(outF
, url
.read())
108 # All done - close the temp file and return its location.
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
)
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.
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()))
137 class PutBackIterator(Iterator
):
138 def __init__(self
, iterable
):
139 self
._iterable
= iter(iterable
)
154 return self
._iterable
.next()
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
174 interp -- The program that should be used to interpret this
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
)
193 """Return a string formatted for output to a kickstart file."""
196 if self
.type == constants
.KS_SCRIPT_PRE
:
198 elif self
.type == constants
.KS_SCRIPT_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
210 retval
+= " --erroronfail"
212 if self
.script
.endswith("\n"):
213 if ver
>= version
.F8
:
214 return retval
+ "\n%s%%end\n" % self
.script
216 return retval
+ "\n%s\n" % self
.script
218 if ver
>= version
.F8
:
219 return retval
+ "\n%s\n%%end\n" % self
.script
221 return retval
+ "\n%s\n" % self
.script
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.
237 self
.include
= include
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
246 return "@%s" % self
.name
248 def __cmp__(self
, other
):
249 if self
.name
< other
.name
:
251 elif self
.name
> other
.name
:
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
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
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.
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
)
295 self
.environment
= None
296 self
.excludedList
= []
297 self
.excludedGroupList
= []
298 self
.excludeDocs
= False
300 self
.handleMissing
= constants
.KS_MISSING_PROMPT
301 self
.packageList
= []
302 self
.instLangs
= None
303 self
.multiLib
= False
307 """Return a string formatted for output to a kickstart file."""
312 pkgs
+= "@^%s\n" % self
.environment
314 grps
= self
.groupList
317 pkgs
+= "%s\n" % grp
.__str
__()
324 grps
= self
.excludedGroupList
327 pkgs
+= "-%s\n" % grp
.__str
__()
329 p
= self
.excludedList
332 pkgs
+= "-%s\n" % pkg
337 retval
= "\n%packages"
340 retval
+= " --default"
342 retval
+= " --excludedocs"
344 retval
+= " --nobase"
346 retval
+= " --nocore"
347 if self
.handleMissing
== constants
.KS_MISSING_IGNORE
:
348 retval
+= " --ignoremissing"
350 retval
+= " --instLangs=%s" % self
.instLangs
352 retval
+= " --multilib"
354 if ver
>= version
.F8
:
355 return retval
+ "\n" + pkgs
+ "\n%end\n"
357 return retval
+ "\n" + pkgs
+ "\n"
359 def _processGroup (self
, line
):
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
371 grp
= " ".join(extra
)
374 self
.groupList
.append(Group(name
=grp
, include
=constants
.GROUP_REQUIRED
))
376 self
.groupList
.append(Group(name
=grp
, include
=constants
.GROUP_ALL
))
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
= []
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:]))
404 newExcludedSet
.add(stripped
[1:])
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
)
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
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
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
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
462 self
.missingIncludeIsFatal
= missingIncludeIsFatal
464 self
._state
= STATE_COMMANDS
465 self
._includeDepth
= 0
468 self
.version
= self
.handler
.version
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.
494 self
.handler
.currentCmd
= args
[0]
495 self
.handler
.currentLine
= self
._line
496 retval
= self
.handler
.dispatcher(args
, lineno
)
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
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
520 self
._state
= STATE_COMMANDS
522 def _handleSpecialComments(self
, line
):
523 """Kickstart recognizes a couple special comments."""
524 if self
._state
!= STATE_COMMANDS
:
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
]
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
))
543 except StopIteration:
548 # Throw away blank lines and comments, unless the section wants all
550 if self
._isBlankOrComment
(line
) and not obj
.allLines
:
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
558 possibleSectionStart
= line
.split()[0]
559 if not self
._validState
(possibleSectionStart
) \
560 and possibleSectionStart
not in ("%end", "%include"):
564 args
= shlex
.split(line
)
566 if args
and args
[0] == "%end":
567 # This is a properly terminated section.
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])
576 elif args
and args
[0] == "%ksappend":
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.
590 # This is just a line within a section. Pass it off to whatever
591 # section handles it.
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.
607 except Exception, msg
:
608 if self
.errorsAreFatal
:
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
:
621 self
._includeDepth
+= 1
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
:
632 self
._includeDepth
-= 1
634 def _stateMachine(self
, lineIter
):
635 # For error reporting.
639 # Get the next line out of the file, quitting if this is the last line.
641 self
._line
= lineIter
.next()
644 except StopIteration:
649 # Eliminate blank lines, whitespace-only lines, and comments.
650 if self
._isBlankOrComment
(self
._line
):
651 self
._handleSpecialComments
(self
._line
)
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])
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.
669 elif args
[0][0] == '%':
670 # This is the beginning of a new section. Handle its header
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
)
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
:
689 elif self
._includeDepth
> 0:
690 lineIter
.put(self
._line
)
692 lineno
= self
._readSection
(lineIter
, lineno
)
694 def readKickstartFromString (self
, s
, reset
=True):
695 """Process a kickstart file, provided as the string str."""
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
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."""
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
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
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
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
))