2 # parser.py: Kickstart file parser.
4 # Chris Lumens <clumens@redhat.com>
6 # Copyright 2005, 2006, 2007, 2008 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.
40 from optparse
import *
41 from urlgrabber
import urlopen
42 import urlgrabber
.grabber
as grabber
44 from constants
import *
51 _
= lambda x
: gettext
.ldgettext("pykickstart", x
)
59 # FIXME: This is a hack until I have time to think about making the parser
60 # itself support multiple syntax versions. Yes, I know this means it will
64 def _preprocessStateMachine (provideLineFn
):
68 # Now open an output kickstart file that we are going to write to one
70 (outF
, outName
) = tempfile
.mkstemp("-ks.cfg", "", "/tmp")
78 # At the end of the file?
86 if string
.find(ll
, "%ksappend") == -1:
90 # Try to pull down the remote file.
92 ksurl
= string
.split(ll
, ' ')[1]
94 raise KickstartParseError
, formatErrorMsg(lineno
, msg
=_("Illegal url for %%ksappend: %s") % ll
)
97 url
= grabber
.urlopen(ksurl
)
98 except grabber
.URLGrabError
, e
:
99 raise KickstartError
, formatErrorMsg(lineno
, msg
=_("Unable to open %%ksappend file: ") % e
.strerror
)
101 # Sanity check result. Sometimes FTP doesn't catch a file
104 if url
.info()["content-length"] < 1:
105 raise KickstartError
, formatErrorMsg(lineno
, msg
=_("Unable to open %%ksappend file"))
107 raise KickstartError
, formatErrorMsg(lineno
, msg
=_("Unable to open %%ksappend file"))
109 # If that worked, write the remote file to the output kickstart
110 # file in one burst. Then close everything up to get ready to
111 # read ahead in the input file. This allows multiple %ksappend
114 os
.write(outF
, url
.read())
117 # All done - close the temp file and return its location.
121 def preprocessFromString (str):
122 """Preprocess the kickstart file, provided as the string str. This
123 method is currently only useful for handling %ksappend lines,
124 which need to be fetched before the real kickstart parser can be
125 run. Returns the location of the complete kickstart file.
127 i
= iter(str.splitlines(True) + [""])
128 rc
= _preprocessStateMachine (lambda: i
.next())
131 def preprocessKickstart (file):
132 """Preprocess the kickstart file, given by the filename file. This
133 method is currently only useful for handling %ksappend lines,
134 which need to be fetched before the real kickstart parser can be
135 run. Returns the location of the complete kickstart file.
138 rc
= _preprocessStateMachine (lambda: fh
.readline())
145 class Script(KickstartObject
):
146 """A class representing a single kickstart script. If functionality beyond
147 just a data representation is needed (for example, a run method in
148 anaconda), Script may be subclassed. Although a run method is not
149 provided, most of the attributes of Script have to do with running the
150 script. Instances of Script are held in a list by the Version object.
152 def __init__(self
, script
, *args
, **kwargs
):
153 """Create a new Script instance. Instance attributes:
155 errorOnFail -- If execution of the script fails, should anaconda
156 stop, display an error, and then reboot without
157 running any other scripts?
158 inChroot -- Does the script execute in anaconda's chroot
160 interp -- The program that should be used to interpret this
162 lineno -- The line number this script starts on.
163 logfile -- Where all messages from the script should be logged.
164 script -- A string containing all the lines of the script.
165 type -- The type of the script, which can be KS_SCRIPT_* from
166 pykickstart.constants.
168 KickstartObject
.__init
__(self
, *args
, **kwargs
)
169 self
.script
= string
.join(script
, "")
171 self
.interp
= kwargs
.get("interp", "/bin/sh")
172 self
.inChroot
= kwargs
.get("inChroot", False)
173 self
.lineno
= kwargs
.get("lineno", None)
174 self
.logfile
= kwargs
.get("logfile", None)
175 self
.errorOnFail
= kwargs
.get("errorOnFail", False)
176 self
.type = kwargs
.get("type", KS_SCRIPT_PRE
)
179 """Return a string formatted for output to a kickstart file."""
180 if self
.preceededInclude
is not None:
181 retval
= "\n%%include %s\n" % self
.preceededInclude
185 if self
.type == KS_SCRIPT_PRE
:
187 elif self
.type == KS_SCRIPT_POST
:
189 elif self
.type == KS_SCRIPT_TRACEBACK
:
190 retval
+= '\n%traceback'
192 if self
.interp
!= "/bin/sh" and self
.interp
!= "":
193 retval
+= " --interpreter=%s" % self
.interp
194 if self
.type == KS_SCRIPT_POST
and not self
.inChroot
:
195 retval
+= " --nochroot"
196 if self
.logfile
!= None:
197 retval
+= " --logfile %s" % self
.logfile
199 retval
+= " --erroronfail"
201 if self
.script
.endswith("\n"):
203 return retval
+ "\n%s%%end\n" % self
.script
205 return retval
+ "\n%s\n" % self
.script
208 return retval
+ "\n%s\n%%end\n" % self
.script
210 return retval
+ "\n%s\n" % self
.script
217 """A class representing a single group in the %packages section."""
218 def __init__(self
, name
="", include
=GROUP_DEFAULT
):
219 """Create a new Group instance. Instance attributes:
221 name -- The group's identifier
222 include -- The level of how much of the group should be included.
223 Values can be GROUP_* from pykickstart.constants.
226 self
.include
= include
229 """Return a string formatted for output to a kickstart file."""
230 if self
.include
== GROUP_REQUIRED
:
231 return "@%s --nodefaults" % self
.name
232 elif self
.include
== GROUP_ALL
:
233 return "@%s --optional" % self
.name
235 return "@%s" % self
.name
237 def __cmp__(self
, other
):
238 if self
.name
< other
.name
:
240 elif self
.name
> other
.name
:
244 class Packages(KickstartObject
):
245 """A class representing the %packages section of the kickstart file."""
246 def __init__(self
, *args
, **kwargs
):
247 """Create a new Packages instance. Instance attributes:
249 addBase -- Should the Base group be installed even if it is
251 default -- Should the default package set be selected?
252 excludedList -- A list of all the packages marked for exclusion in
253 the %packages section, without the leading minus
255 excludeDocs -- Should documentation in each package be excluded?
256 groupList -- A list of Group objects representing all the groups
257 specified in the %packages section. Names will be
258 stripped of the leading @ symbol.
259 handleMissing -- If unknown packages are specified in the %packages
260 section, should it be ignored or not? Values can
261 be KS_MISSING_* from pykickstart.constants.
262 packageList -- A list of all the packages specified in the
264 instLangs -- A list of languages to install.
266 KickstartObject
.__init
__(self
, *args
, **kwargs
)
270 self
.excludedList
= []
271 self
.excludeDocs
= False
273 self
.handleMissing
= KS_MISSING_PROMPT
274 self
.packageList
= []
275 self
.instLangs
= None
278 """Return a string formatted for output to a kickstart file."""
282 grps
= self
.groupList
285 pkgs
+= "%s\n" % grp
.__str
__()
292 p
= self
.excludedList
295 pkgs
+= "-%s\n" % pkg
300 if self
.preceededInclude
is not None:
301 retval
= "\n%%include %s\n" % self
.preceededInclude
305 retval
+= "\n%packages"
308 retval
+= " --default"
310 retval
+= " --excludedocs"
312 retval
+= " --nobase"
313 if self
.handleMissing
== KS_MISSING_IGNORE
:
314 retval
+= " --ignoremissing"
316 retval
+= " --instLangs=%s" % self
.instLangs
319 return retval
+ "\n" + pkgs
+ "\n%end\n"
321 return retval
+ "\n" + pkgs
+ "\n"
323 def _processGroup (self
, line
):
325 op
.add_option("--nodefaults", action
="store_true", default
=False)
326 op
.add_option("--optional", action
="store_true", default
=False)
328 (opts
, extra
) = op
.parse_args(args
=line
.split())
330 if opts
.nodefaults
and opts
.optional
:
331 raise KickstartValueError
, _("Group cannot specify both --nodefaults and --optional")
333 # If the group name has spaces in it, we have to put it back together
335 grp
= " ".join(extra
)
338 self
.groupList
.append(Group(name
=grp
, include
=GROUP_REQUIRED
))
340 self
.groupList
.append(Group(name
=grp
, include
=GROUP_ALL
))
342 self
.groupList
.append(Group(name
=grp
, include
=GROUP_DEFAULT
))
344 def add (self
, pkgList
):
345 """Given a list of lines from the input file, strip off any leading
346 symbols and add the result to the appropriate list.
348 existingExcludedSet
= set(self
.excludedList
)
349 existingPackageSet
= set(self
.packageList
)
350 newExcludedSet
= set()
351 newPackageSet
= set()
354 stripped
= pkg
.strip()
356 if stripped
[0] == "@":
357 self
._processGroup
(stripped
[1:])
358 elif stripped
[0] == "-":
359 # Support syntax for removing a previously included group. If
360 # the provided group does not exist, it's not an error.
361 if stripped
[1] == "@":
363 self
.groupList
= filter(lambda g
: g
.name
!= stripped
[2:], self
.groupList
)
367 newExcludedSet
.add(stripped
[1:])
369 newPackageSet
.add(stripped
)
371 existingPackageSet
= (existingPackageSet
- newExcludedSet
) | newPackageSet
372 existingExcludedSet
= (existingExcludedSet
- existingPackageSet
) | newExcludedSet
374 self
.packageList
= list(existingPackageSet
)
375 self
.excludedList
= list(existingExcludedSet
)
381 class KickstartParser
:
382 """The kickstart file parser class as represented by a basic state
383 machine. To create a specialized parser, make a subclass and override
384 any of the methods you care about. Methods that don't need to do
385 anything may just pass. However, _stateMachine should never be
388 def __init__ (self
, handler
, followIncludes
=True, errorsAreFatal
=True,
389 missingIncludeIsFatal
=True):
390 """Create a new KickstartParser instance. Instance attributes:
392 errorsAreFatal -- Should errors cause processing to halt, or
393 just print a message to the screen? This
394 is most useful for writing syntax checkers
395 that may want to continue after an error is
397 followIncludes -- If %include is seen, should the included
398 file be checked as well or skipped?
399 handler -- An instance of a BaseHandler subclass. If
400 None, the input file will still be parsed
401 but no data will be saved and no commands
403 missingIncludeIsFatal -- Should missing include files be fatal, even
404 if errorsAreFatal is False?
406 self
.errorsAreFatal
= errorsAreFatal
407 self
.followIncludes
= followIncludes
408 self
.handler
= handler
410 self
.missingIncludeIsFatal
= missingIncludeIsFatal
415 self
.version
= self
.handler
.version
421 """Reset the internal variables of the state machine for a new kickstart file."""
422 self
._state
= STATE_COMMANDS
424 self
._includeDepth
= 0
425 self
._preceededInclude
= None
427 def addScript (self
):
428 """Create a new Script instance and add it to the Version object. This
429 is called when the end of a script section is seen and may be
430 overridden in a subclass if necessary.
432 if string
.join(self
._script
["body"]).strip() == "":
435 kwargs
= {"interp": self
._script
["interp"],
436 "inChroot": self
._script
["chroot"],
437 "lineno": self
._script
["lineno"],
438 "logfile": self
._script
["log"],
439 "errorOnFail": self
._script
["errorOnFail"],
440 "type": self
._script
["type"]}
442 if self
._preceededInclude
is not None:
443 kwargs
["preceededInclude"] = self
._preceededInclude
444 self
._preceededInclude
= None
446 s
= Script (self
._script
["body"], **kwargs
)
449 self
.handler
.scripts
.append(s
)
451 def addPackages (self
, line
):
452 """Add the single package, exclude, or group into the Version's
453 Packages instance. This method may be overridden in a subclass
457 self
.handler
.packages
.add([line
])
459 def handleCommand (self
, lineno
, args
):
460 """Given the list of command and arguments, call the Version's
461 dispatcher method to handle the command. This method may be
462 overridden in a subclass if necessary.
465 self
.handler
.currentCmd
= args
[0]
466 self
.handler
.currentLine
= self
._line
467 self
.handler
.dispatcher(args
, lineno
, self
._preceededInclude
)
468 self
._preceededInclude
= None
470 def handlePackageHdr (self
, lineno
, args
):
471 """Process the arguments to the %packages header and set attributes
472 on the Version's Packages instance appropriate. This method may be
473 overridden in a subclass if necessary.
475 op
= KSOptionParser(version
=self
.version
)
476 op
.add_option("--excludedocs", dest
="excludedocs", action
="store_true",
478 op
.add_option("--ignoremissing", dest
="ignoremissing",
479 action
="store_true", default
=False)
480 op
.add_option("--nobase", dest
="nobase", action
="store_true",
482 op
.add_option("--ignoredeps", dest
="resolveDeps", action
="store_false",
483 deprecated
=FC4
, removed
=F9
)
484 op
.add_option("--resolvedeps", dest
="resolveDeps", action
="store_true",
485 deprecated
=FC4
, removed
=F9
)
486 op
.add_option("--default", dest
="defaultPackages", action
="store_true",
487 default
=False, introduced
=F7
)
488 op
.add_option("--instLangs", dest
="instLangs", type="string",
489 default
="", introduced
=F9
)
491 (opts
, extra
) = op
.parse_args(args
=args
[1:], lineno
=lineno
)
493 self
.handler
.packages
.excludeDocs
= opts
.excludedocs
494 self
.handler
.packages
.addBase
= not opts
.nobase
495 if opts
.ignoremissing
:
496 self
.handler
.packages
.handleMissing
= KS_MISSING_IGNORE
498 self
.handler
.packages
.handleMissing
= KS_MISSING_PROMPT
500 if opts
.defaultPackages
:
501 self
.handler
.packages
.default
= True
504 self
.handler
.packages
.instLangs
= opts
.instLangs
506 if self
._preceededInclude
is not None:
507 self
.handler
.packages
.preceededInclude
= self
._preceededInclude
508 self
._preceededInclude
= None
510 def handleScriptHdr (self
, lineno
, args
):
511 """Process the arguments to a %pre/%post/%traceback header for later
512 setting on a Script instance once the end of the script is found.
513 This method may be overridden in a subclass if necessary.
515 op
= KSOptionParser(version
=self
.version
)
516 op
.add_option("--erroronfail", dest
="errorOnFail", action
="store_true",
518 op
.add_option("--interpreter", dest
="interpreter", default
="/bin/sh")
519 op
.add_option("--log", "--logfile", dest
="log")
521 if args
[0] == "%pre" or args
[0] == "%traceback":
522 self
._script
["chroot"] = False
523 elif args
[0] == "%post":
524 self
._script
["chroot"] = True
525 op
.add_option("--nochroot", dest
="nochroot", action
="store_true",
528 (opts
, extra
) = op
.parse_args(args
=args
[1:], lineno
=lineno
)
530 self
._script
["interp"] = opts
.interpreter
531 self
._script
["lineno"] = lineno
532 self
._script
["log"] = opts
.log
533 self
._script
["errorOnFail"] = opts
.errorOnFail
534 if hasattr(opts
, "nochroot"):
535 self
._script
["chroot"] = not opts
.nochroot
537 def _stateMachine (self
, provideLineFn
):
538 # For error reporting.
545 self
._line
= provideLineFn()
546 except StopIteration:
552 # At the end of an included file
553 if self
._line
== "" and self
._includeDepth
> 0:
556 # Don't eliminate whitespace or comments from scripts.
557 if self
._line
.isspace() or (self
._line
!= "" and self
._line
.lstrip()[0] == '#'):
558 # Save the platform for s-c-kickstart, though.
559 if self
._line
[:10] == "#platform=" and self
._state
== STATE_COMMANDS
:
560 self
.handler
.platform
= self
._line
[11:]
562 if self
._state
== STATE_SCRIPT
:
563 self
._script
["body"].append(self
._line
)
568 # We only want to split the line if we're outside of a script,
569 # as inside the script might involve some pretty weird quoting
570 # that shlex doesn't understand.
571 if self
._state
== STATE_SCRIPT
:
572 # Have we found a state transition? If so, we still want
573 # to split. Otherwise, args won't be set but we'll fall through
574 # all the way to the last case.
575 if self
._line
!= "" and string
.split(self
._line
.lstrip())[0] in \
576 ["%end", "%post", "%pre", "%traceback", "%include", "%packages", "%ksappend"]:
577 args
= shlex
.split(self
._line
)
581 # Remove any end-of-line comments.
582 ind
= self
._line
.find("#")
588 self
._line
= h
.rstrip()
589 args
= shlex
.split(self
._line
)
591 if args
and args
[0] == "%include":
592 self
._preceededInclude
= args
[1]
594 # This case comes up primarily in ksvalidator.
595 if not self
.followIncludes
:
600 raise KickstartParseError
, formatErrorMsg(lineno
)
602 self
._includeDepth
+= 1
605 self
.readKickstart (args
[1], reset
=False)
607 # Handle the include file being provided over the
608 # network in a %pre script. This case comes up in the
609 # early parsing in anaconda.
610 if self
.missingIncludeIsFatal
:
613 self
._includeDepth
-= 1
617 if self
._state
== STATE_COMMANDS
:
618 if not args
and self
._includeDepth
== 0:
619 self
._state
= STATE_END
620 elif args
[0] == "%ksappend":
622 elif args
[0] in ["%pre", "%post", "%traceback"]:
623 self
._state
= STATE_SCRIPT_HDR
624 elif args
[0] == "%packages":
625 self
._state
= STATE_PACKAGES
626 elif args
[0][0] == '%':
627 # This error is too difficult to continue from, without
628 # lots of resync code. So just print this one and quit.
629 raise KickstartParseError
, formatErrorMsg(lineno
)
633 if self
.errorsAreFatal
:
634 self
.handleCommand(lineno
, args
)
637 self
.handleCommand(lineno
, args
)
638 except Exception, msg
:
641 elif self
._state
== STATE_PACKAGES
:
642 if not args
and self
._includeDepth
== 0:
643 if self
.version
>= F8
:
644 warnings
.warn(_("%s does not end with %%end. This syntax has been deprecated. It may be removed from future releases, which will result in a fatal error from kickstart. Please modify your kickstart file to use this updated syntax.") % "%packages", DeprecationWarning)
646 self
._state
= STATE_END
647 elif args
[0] == "%end":
648 self
._state
= STATE_COMMANDS
650 elif args
[0] == "%ksappend":
652 elif args
[0] in ["%pre", "%post", "%traceback"]:
653 self
._state
= STATE_SCRIPT_HDR
654 elif args
[0] == "%packages":
657 if self
.errorsAreFatal
:
658 self
.handlePackageHdr (lineno
, args
)
661 self
.handlePackageHdr (lineno
, args
)
662 except Exception, msg
:
664 elif args
[0][0] == '%':
665 # This error is too difficult to continue from, without
666 # lots of resync code. So just print this one and quit.
667 raise KickstartParseError
, formatErrorMsg(lineno
)
670 self
.addPackages (string
.rstrip(self
._line
))
672 elif self
._state
== STATE_SCRIPT_HDR
:
674 self
._script
= {"body": [], "interp": "/bin/sh", "log": None,
675 "errorOnFail": False, lineno
: None}
677 if not args
and self
._includeDepth
== 0:
678 self
._state
= STATE_END
679 elif args
[0] == "%pre":
680 self
._state
= STATE_SCRIPT
681 self
._script
["type"] = KS_SCRIPT_PRE
682 elif args
[0] == "%post":
683 self
._state
= STATE_SCRIPT
684 self
._script
["type"] = KS_SCRIPT_POST
685 elif args
[0] == "%traceback":
686 self
._state
= STATE_SCRIPT
687 self
._script
["type"] = KS_SCRIPT_TRACEBACK
688 elif args
[0][0] == '%':
689 # This error is too difficult to continue from, without
690 # lots of resync code. So just print this one and quit.
691 raise KickstartParseError
, formatErrorMsg(lineno
)
693 if self
.errorsAreFatal
:
694 self
.handleScriptHdr (lineno
, args
)
697 self
.handleScriptHdr (lineno
, args
)
698 except Exception, msg
:
701 elif self
._state
== STATE_SCRIPT
:
702 if self
._line
in ["%end", ""] and self
._includeDepth
== 0:
703 if self
._line
== "" and self
.version
>= F8
:
704 warnings
.warn(_("%s does not end with %%end. This syntax has been deprecated. It may be removed from future releases, which will result in a fatal error from kickstart. Please modify your kickstart file to use this updated syntax.") % _("Script"), DeprecationWarning)
706 # If we're at the end of the kickstart file, add the script.
708 self
._state
= STATE_END
709 elif args
and args
[0] in ["%end", "%pre", "%post", "%traceback", "%packages", "%ksappend"]:
710 # Otherwise we're now at the start of the next section.
711 # Figure out what kind of a script we just finished
712 # reading, add it to the list, and switch to the initial
715 self
._state
= STATE_COMMANDS
717 if args
[0] == "%end":
720 # Otherwise just add to the current script body.
721 self
._script
["body"].append(self
._line
)
724 elif self
._state
== STATE_END
:
727 def readKickstartFromString (self
, str, reset
=True):
728 """Process a kickstart file, provided as the string str."""
732 # Add a "" to the end of the list so the string reader acts like the
733 # file reader and we only get StopIteration when we're after the final
735 i
= iter(str.splitlines(True) + [""])
736 self
._stateMachine
(lambda: i
.next())
738 def readKickstart(self
, f
, reset
=True):
739 """Process a kickstart file, given by the filename f."""
743 # an %include might not specify a full path. if we don't try to figure
744 # out what the path should have been, then we're unable to find it
745 # requiring full path specification, though, sucks. so let's make
746 # the reading "smart" by keeping track of what the path is at each
748 if not os
.path
.exists(f
):
749 if self
.currentdir
.has_key(self
._includeDepth
- 1):
750 if os
.path
.exists(os
.path
.join(self
.currentdir
[self
._includeDepth
- 1], f
)):
751 f
= os
.path
.join(self
.currentdir
[self
._includeDepth
- 1], f
)
753 cd
= os
.path
.dirname(f
)
754 if not cd
.startswith("/"):
755 cd
= os
.path
.abspath(cd
)
756 self
.currentdir
[self
._includeDepth
] = cd
759 self
._stateMachine
(lambda: fh
.readline())