summaryrefslogtreecommitdiffstats
path: root/scripts/lib/wic/3rdparty/pykickstart/parser.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/lib/wic/3rdparty/pykickstart/parser.py')
-rw-r--r--scripts/lib/wic/3rdparty/pykickstart/parser.py619
1 files changed, 619 insertions, 0 deletions
diff --git a/scripts/lib/wic/3rdparty/pykickstart/parser.py b/scripts/lib/wic/3rdparty/pykickstart/parser.py
new file mode 100644
index 0000000000..9c9674bf73
--- /dev/null
+++ b/scripts/lib/wic/3rdparty/pykickstart/parser.py
@@ -0,0 +1,619 @@
1#
2# parser.py: Kickstart file parser.
3#
4# Chris Lumens <clumens@redhat.com>
5#
6# Copyright 2005, 2006, 2007, 2008, 2011 Red Hat, Inc.
7#
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.
14#
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.
21#
22"""
23Main kickstart file processing module.
24
25This module exports several important classes:
26
27 Script - Representation of a single %pre, %post, or %traceback script.
28
29 Packages - Representation of the %packages section.
30
31 KickstartParser - The kickstart file parser state machine.
32"""
33
34from collections import Iterator
35import os
36import shlex
37import sys
38import tempfile
39from copy import copy
40from optparse import *
41
42import constants
43from errors import KickstartError, KickstartParseError, KickstartValueError, formatErrorMsg
44from ko import KickstartObject
45from sections import *
46import version
47
48import gettext
49_ = lambda x: gettext.ldgettext("pykickstart", x)
50
51STATE_END = "end"
52STATE_COMMANDS = "commands"
53
54ver = version.DEVEL
55
56
57class PutBackIterator(Iterator):
58 def __init__(self, iterable):
59 self._iterable = iter(iterable)
60 self._buf = None
61
62 def __iter__(self):
63 return self
64
65 def put(self, s):
66 self._buf = s
67
68 def next(self):
69 if self._buf:
70 retval = self._buf
71 self._buf = None
72 return retval
73 else:
74 return self._iterable.next()
75
76###
77### SCRIPT HANDLING
78###
79class Script(KickstartObject):
80 """A class representing a single kickstart script. If functionality beyond
81 just a data representation is needed (for example, a run method in
82 anaconda), Script may be subclassed. Although a run method is not
83 provided, most of the attributes of Script have to do with running the
84 script. Instances of Script are held in a list by the Version object.
85 """
86 def __init__(self, script, *args , **kwargs):
87 """Create a new Script instance. Instance attributes:
88
89 errorOnFail -- If execution of the script fails, should anaconda
90 stop, display an error, and then reboot without
91 running any other scripts?
92 inChroot -- Does the script execute in anaconda's chroot
93 environment or not?
94 interp -- The program that should be used to interpret this
95 script.
96 lineno -- The line number this script starts on.
97 logfile -- Where all messages from the script should be logged.
98 script -- A string containing all the lines of the script.
99 type -- The type of the script, which can be KS_SCRIPT_* from
100 pykickstart.constants.
101 """
102 KickstartObject.__init__(self, *args, **kwargs)
103 self.script = "".join(script)
104
105 self.interp = kwargs.get("interp", "/bin/sh")
106 self.inChroot = kwargs.get("inChroot", False)
107 self.lineno = kwargs.get("lineno", None)
108 self.logfile = kwargs.get("logfile", None)
109 self.errorOnFail = kwargs.get("errorOnFail", False)
110 self.type = kwargs.get("type", constants.KS_SCRIPT_PRE)
111
112 def __str__(self):
113 """Return a string formatted for output to a kickstart file."""
114 retval = ""
115
116 if self.type == constants.KS_SCRIPT_PRE:
117 retval += '\n%pre'
118 elif self.type == constants.KS_SCRIPT_POST:
119 retval += '\n%post'
120 elif self.type == constants.KS_SCRIPT_TRACEBACK:
121 retval += '\n%traceback'
122
123 if self.interp != "/bin/sh" and self.interp != "":
124 retval += " --interpreter=%s" % self.interp
125 if self.type == constants.KS_SCRIPT_POST and not self.inChroot:
126 retval += " --nochroot"
127 if self.logfile != None:
128 retval += " --logfile %s" % self.logfile
129 if self.errorOnFail:
130 retval += " --erroronfail"
131
132 if self.script.endswith("\n"):
133 if ver >= version.F8:
134 return retval + "\n%s%%end\n" % self.script
135 else:
136 return retval + "\n%s\n" % self.script
137 else:
138 if ver >= version.F8:
139 return retval + "\n%s\n%%end\n" % self.script
140 else:
141 return retval + "\n%s\n" % self.script
142
143
144##
145## PACKAGE HANDLING
146##
147class Group:
148 """A class representing a single group in the %packages section."""
149 def __init__(self, name="", include=constants.GROUP_DEFAULT):
150 """Create a new Group instance. Instance attributes:
151
152 name -- The group's identifier
153 include -- The level of how much of the group should be included.
154 Values can be GROUP_* from pykickstart.constants.
155 """
156 self.name = name
157 self.include = include
158
159 def __str__(self):
160 """Return a string formatted for output to a kickstart file."""
161 if self.include == constants.GROUP_REQUIRED:
162 return "@%s --nodefaults" % self.name
163 elif self.include == constants.GROUP_ALL:
164 return "@%s --optional" % self.name
165 else:
166 return "@%s" % self.name
167
168 def __cmp__(self, other):
169 if self.name < other.name:
170 return -1
171 elif self.name > other.name:
172 return 1
173 return 0
174
175class Packages(KickstartObject):
176 """A class representing the %packages section of the kickstart file."""
177 def __init__(self, *args, **kwargs):
178 """Create a new Packages instance. Instance attributes:
179
180 addBase -- Should the Base group be installed even if it is
181 not specified?
182 default -- Should the default package set be selected?
183 excludedList -- A list of all the packages marked for exclusion in
184 the %packages section, without the leading minus
185 symbol.
186 excludeDocs -- Should documentation in each package be excluded?
187 groupList -- A list of Group objects representing all the groups
188 specified in the %packages section. Names will be
189 stripped of the leading @ symbol.
190 excludedGroupList -- A list of Group objects representing all the
191 groups specified for removal in the %packages
192 section. Names will be stripped of the leading
193 -@ symbols.
194 handleMissing -- If unknown packages are specified in the %packages
195 section, should it be ignored or not? Values can
196 be KS_MISSING_* from pykickstart.constants.
197 packageList -- A list of all the packages specified in the
198 %packages section.
199 instLangs -- A list of languages to install.
200 """
201 KickstartObject.__init__(self, *args, **kwargs)
202
203 self.addBase = True
204 self.default = False
205 self.excludedList = []
206 self.excludedGroupList = []
207 self.excludeDocs = False
208 self.groupList = []
209 self.handleMissing = constants.KS_MISSING_PROMPT
210 self.packageList = []
211 self.instLangs = None
212
213 def __str__(self):
214 """Return a string formatted for output to a kickstart file."""
215 pkgs = ""
216
217 if not self.default:
218 grps = self.groupList
219 grps.sort()
220 for grp in grps:
221 pkgs += "%s\n" % grp.__str__()
222
223 p = self.packageList
224 p.sort()
225 for pkg in p:
226 pkgs += "%s\n" % pkg
227
228 grps = self.excludedGroupList
229 grps.sort()
230 for grp in grps:
231 pkgs += "-%s\n" % grp.__str__()
232
233 p = self.excludedList
234 p.sort()
235 for pkg in p:
236 pkgs += "-%s\n" % pkg
237
238 if pkgs == "":
239 return ""
240
241 retval = "\n%packages"
242
243 if self.default:
244 retval += " --default"
245 if self.excludeDocs:
246 retval += " --excludedocs"
247 if not self.addBase:
248 retval += " --nobase"
249 if self.handleMissing == constants.KS_MISSING_IGNORE:
250 retval += " --ignoremissing"
251 if self.instLangs:
252 retval += " --instLangs=%s" % self.instLangs
253
254 if ver >= version.F8:
255 return retval + "\n" + pkgs + "\n%end\n"
256 else:
257 return retval + "\n" + pkgs + "\n"
258
259 def _processGroup (self, line):
260 op = OptionParser()
261 op.add_option("--nodefaults", action="store_true", default=False)
262 op.add_option("--optional", action="store_true", default=False)
263
264 (opts, extra) = op.parse_args(args=line.split())
265
266 if opts.nodefaults and opts.optional:
267 raise KickstartValueError, _("Group cannot specify both --nodefaults and --optional")
268
269 # If the group name has spaces in it, we have to put it back together
270 # now.
271 grp = " ".join(extra)
272
273 if opts.nodefaults:
274 self.groupList.append(Group(name=grp, include=constants.GROUP_REQUIRED))
275 elif opts.optional:
276 self.groupList.append(Group(name=grp, include=constants.GROUP_ALL))
277 else:
278 self.groupList.append(Group(name=grp, include=constants.GROUP_DEFAULT))
279
280 def add (self, pkgList):
281 """Given a list of lines from the input file, strip off any leading
282 symbols and add the result to the appropriate list.
283 """
284 existingExcludedSet = set(self.excludedList)
285 existingPackageSet = set(self.packageList)
286 newExcludedSet = set()
287 newPackageSet = set()
288
289 excludedGroupList = []
290
291 for pkg in pkgList:
292 stripped = pkg.strip()
293
294 if stripped[0] == "@":
295 self._processGroup(stripped[1:])
296 elif stripped[0] == "-":
297 if stripped[1] == "@":
298 excludedGroupList.append(Group(name=stripped[2:]))
299 else:
300 newExcludedSet.add(stripped[1:])
301 else:
302 newPackageSet.add(stripped)
303
304 # Groups have to be excluded in two different ways (note: can't use
305 # sets here because we have to store objects):
306 excludedGroupNames = map(lambda g: g.name, excludedGroupList)
307
308 # First, an excluded group may be cancelling out a previously given
309 # one. This is often the case when using %include. So there we should
310 # just remove the group from the list.
311 self.groupList = filter(lambda g: g.name not in excludedGroupNames, self.groupList)
312
313 # Second, the package list could have included globs which are not
314 # processed by pykickstart. In that case we need to preserve a list of
315 # excluded groups so whatever tool doing package/group installation can
316 # take appropriate action.
317 self.excludedGroupList.extend(excludedGroupList)
318
319 existingPackageSet = (existingPackageSet - newExcludedSet) | newPackageSet
320 existingExcludedSet = (existingExcludedSet - existingPackageSet) | newExcludedSet
321
322 self.packageList = list(existingPackageSet)
323 self.excludedList = list(existingExcludedSet)
324
325
326###
327### PARSER
328###
329class KickstartParser:
330 """The kickstart file parser class as represented by a basic state
331 machine. To create a specialized parser, make a subclass and override
332 any of the methods you care about. Methods that don't need to do
333 anything may just pass. However, _stateMachine should never be
334 overridden.
335 """
336 def __init__ (self, handler, followIncludes=True, errorsAreFatal=True,
337 missingIncludeIsFatal=True):
338 """Create a new KickstartParser instance. Instance attributes:
339
340 errorsAreFatal -- Should errors cause processing to halt, or
341 just print a message to the screen? This
342 is most useful for writing syntax checkers
343 that may want to continue after an error is
344 encountered.
345 followIncludes -- If %include is seen, should the included
346 file be checked as well or skipped?
347 handler -- An instance of a BaseHandler subclass. If
348 None, the input file will still be parsed
349 but no data will be saved and no commands
350 will be executed.
351 missingIncludeIsFatal -- Should missing include files be fatal, even
352 if errorsAreFatal is False?
353 """
354 self.errorsAreFatal = errorsAreFatal
355 self.followIncludes = followIncludes
356 self.handler = handler
357 self.currentdir = {}
358 self.missingIncludeIsFatal = missingIncludeIsFatal
359
360 self._state = STATE_COMMANDS
361 self._includeDepth = 0
362 self._line = ""
363
364 self.version = self.handler.version
365
366 global ver
367 ver = self.version
368
369 self._sections = {}
370 self.setupSections()
371
372 def _reset(self):
373 """Reset the internal variables of the state machine for a new kickstart file."""
374 self._state = STATE_COMMANDS
375 self._includeDepth = 0
376
377 def getSection(self, s):
378 """Return a reference to the requested section (s must start with '%'s),
379 or raise KeyError if not found.
380 """
381 return self._sections[s]
382
383 def handleCommand (self, lineno, args):
384 """Given the list of command and arguments, call the Version's
385 dispatcher method to handle the command. Returns the command or
386 data object returned by the dispatcher. This method may be
387 overridden in a subclass if necessary.
388 """
389 if self.handler:
390 self.handler.currentCmd = args[0]
391 self.handler.currentLine = self._line
392 retval = self.handler.dispatcher(args, lineno)
393
394 return retval
395
396 def registerSection(self, obj):
397 """Given an instance of a Section subclass, register the new section
398 with the parser. Calling this method means the parser will
399 recognize your new section and dispatch into the given object to
400 handle it.
401 """
402 if not obj.sectionOpen:
403 raise TypeError, "no sectionOpen given for section %s" % obj
404
405 if not obj.sectionOpen.startswith("%"):
406 raise TypeError, "section %s tag does not start with a %%" % obj.sectionOpen
407
408 self._sections[obj.sectionOpen] = obj
409
410 def _finalize(self, obj):
411 """Called at the close of a kickstart section to take any required
412 actions. Internally, this is used to add scripts once we have the
413 whole body read.
414 """
415 obj.finalize()
416 self._state = STATE_COMMANDS
417
418 def _handleSpecialComments(self, line):
419 """Kickstart recognizes a couple special comments."""
420 if self._state != STATE_COMMANDS:
421 return
422
423 # Save the platform for s-c-kickstart.
424 if line[:10] == "#platform=":
425 self.handler.platform = self._line[11:]
426
427 def _readSection(self, lineIter, lineno):
428 obj = self._sections[self._state]
429
430 while True:
431 try:
432 line = lineIter.next()
433 if line == "":
434 # This section ends at the end of the file.
435 if self.version >= version.F8:
436 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Section does not end with %%end."))
437
438 self._finalize(obj)
439 except StopIteration:
440 break
441
442 lineno += 1
443
444 # Throw away blank lines and comments, unless the section wants all
445 # lines.
446 if self._isBlankOrComment(line) and not obj.allLines:
447 continue
448
449 if line.startswith("%"):
450 args = shlex.split(line)
451
452 if args and args[0] == "%end":
453 # This is a properly terminated section.
454 self._finalize(obj)
455 break
456 elif args and args[0] == "%ksappend":
457 continue
458 elif args and (self._validState(args[0]) or args[0] in ["%include", "%ksappend"]):
459 # This is an unterminated section.
460 if self.version >= version.F8:
461 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Section does not end with %%end."))
462
463 # Finish up. We do not process the header here because
464 # kicking back out to STATE_COMMANDS will ensure that happens.
465 lineIter.put(line)
466 lineno -= 1
467 self._finalize(obj)
468 break
469 else:
470 # This is just a line within a section. Pass it off to whatever
471 # section handles it.
472 obj.handleLine(line)
473
474 return lineno
475
476 def _validState(self, st):
477 """Is the given section tag one that has been registered with the parser?"""
478 return st in self._sections.keys()
479
480 def _tryFunc(self, fn):
481 """Call the provided function (which doesn't take any arguments) and
482 do the appropriate error handling. If errorsAreFatal is False, this
483 function will just print the exception and keep going.
484 """
485 try:
486 fn()
487 except Exception, msg:
488 if self.errorsAreFatal:
489 raise
490 else:
491 print msg
492
493 def _isBlankOrComment(self, line):
494 return line.isspace() or line == "" or line.lstrip()[0] == '#'
495
496 def _stateMachine(self, lineIter):
497 # For error reporting.
498 lineno = 0
499
500 while True:
501 # Get the next line out of the file, quitting if this is the last line.
502 try:
503 self._line = lineIter.next()
504 if self._line == "":
505 break
506 except StopIteration:
507 break
508
509 lineno += 1
510
511 # Eliminate blank lines, whitespace-only lines, and comments.
512 if self._isBlankOrComment(self._line):
513 self._handleSpecialComments(self._line)
514 continue
515
516 # Remove any end-of-line comments.
517 sanitized = self._line.split("#")[0]
518
519 # Then split the line.
520 args = shlex.split(sanitized.rstrip())
521
522 if args[0] == "%include":
523 # This case comes up primarily in ksvalidator.
524 if not self.followIncludes:
525 continue
526
527 if len(args) == 1 or not args[1]:
528 raise KickstartParseError, formatErrorMsg(lineno)
529
530 self._includeDepth += 1
531
532 try:
533 self.readKickstart(args[1], reset=False)
534 except KickstartError:
535 # Handle the include file being provided over the
536 # network in a %pre script. This case comes up in the
537 # early parsing in anaconda.
538 if self.missingIncludeIsFatal:
539 raise
540
541 self._includeDepth -= 1
542 continue
543
544 # Now on to the main event.
545 if self._state == STATE_COMMANDS:
546 if args[0] == "%ksappend":
547 # This is handled by the preprocess* functions, so continue.
548 continue
549 elif args[0][0] == '%':
550 # This is the beginning of a new section. Handle its header
551 # here.
552 newSection = args[0]
553 if not self._validState(newSection):
554 raise KickstartParseError, formatErrorMsg(lineno, msg=_("Unknown kickstart section: %s" % newSection))
555
556 self._state = newSection
557 obj = self._sections[self._state]
558 self._tryFunc(lambda: obj.handleHeader(lineno, args))
559
560 # This will handle all section processing, kicking us back
561 # out to STATE_COMMANDS at the end with the current line
562 # being the next section header, etc.
563 lineno = self._readSection(lineIter, lineno)
564 else:
565 # This is a command in the command section. Dispatch to it.
566 self._tryFunc(lambda: self.handleCommand(lineno, args))
567 elif self._state == STATE_END:
568 break
569
570 def readKickstartFromString (self, s, reset=True):
571 """Process a kickstart file, provided as the string str."""
572 if reset:
573 self._reset()
574
575 # Add a "" to the end of the list so the string reader acts like the
576 # file reader and we only get StopIteration when we're after the final
577 # line of input.
578 i = PutBackIterator(s.splitlines(True) + [""])
579 self._stateMachine (i)
580
581 def readKickstart(self, f, reset=True):
582 """Process a kickstart file, given by the filename f."""
583 if reset:
584 self._reset()
585
586 # an %include might not specify a full path. if we don't try to figure
587 # out what the path should have been, then we're unable to find it
588 # requiring full path specification, though, sucks. so let's make
589 # the reading "smart" by keeping track of what the path is at each
590 # include depth.
591 if not os.path.exists(f):
592 if self.currentdir.has_key(self._includeDepth - 1):
593 if os.path.exists(os.path.join(self.currentdir[self._includeDepth - 1], f)):
594 f = os.path.join(self.currentdir[self._includeDepth - 1], f)
595
596 cd = os.path.dirname(f)
597 if not cd.startswith("/"):
598 cd = os.path.abspath(cd)
599 self.currentdir[self._includeDepth] = cd
600
601 try:
602 s = file(f).read()
603 except IOError, e:
604 raise KickstartError, formatErrorMsg(0, msg=_("Unable to open input kickstart file: %s") % e.strerror)
605
606 self.readKickstartFromString(s, reset=False)
607
608 def setupSections(self):
609 """Install the sections all kickstart files support. You may override
610 this method in a subclass, but should avoid doing so unless you know
611 what you're doing.
612 """
613 self._sections = {}
614
615 # Install the sections all kickstart files support.
616 self.registerSection(PreScriptSection(self.handler, dataObj=Script))
617 self.registerSection(PostScriptSection(self.handler, dataObj=Script))
618 self.registerSection(TracebackScriptSection(self.handler, dataObj=Script))
619 self.registerSection(PackageSection(self.handler))