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))