~registry/pykickstart/trunk

« back to all changes in this revision

Viewing changes to pykickstart/parser.py

  • Committer: Chris Lumens
  • Date: 2014-10-22 14:19:44 UTC
  • Revision ID: git-v1:982498c0647c4bc3b4460209d4a76698c0eb6f8c
Add a note that the repo has moved.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
 
"""
23
 
Main kickstart file processing module.
24
 
 
25
 
This 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
 
 
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
41
 
 
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
46
 
 
47
 
import gettext
48
 
_ = lambda x: gettext.ldgettext("pykickstart", x)
49
 
 
50
 
STATE_END = "end"
51
 
STATE_COMMANDS = "commands"
52
 
 
53
 
ver = version.DEVEL
54
 
 
55
 
def _preprocessStateMachine (lineIter):
56
 
    l = None
57
 
    lineno = 0
58
 
 
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")
62
 
 
63
 
    while True:
64
 
        try:
65
 
            l = lineIter.next()
66
 
        except StopIteration:
67
 
            break
68
 
 
69
 
        # At the end of the file?
70
 
        if l == "":
71
 
            break
72
 
 
73
 
        lineno += 1
74
 
        url = None
75
 
 
76
 
        ll = l.strip()
77
 
        if not ll.startswith("%ksappend"):
78
 
            os.write(outF, l)
79
 
            continue
80
 
 
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))
86
 
 
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")))
99
 
 
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()
107
 
 
108
 
    # All done - close the temp file and return its location.
109
 
    os.close(outF)
110
 
    return outName
111
 
 
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.
117
 
    """
118
 
    i = iter(s.splitlines(True) + [""])
119
 
    rc = _preprocessStateMachine (i.next)
120
 
    return rc
121
 
 
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.
127
 
    """
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))
132
 
 
133
 
    rc = _preprocessStateMachine (iter(fh.readlines()))
134
 
    fh.close()
135
 
    return rc
136
 
 
137
 
class PutBackIterator(Iterator):
138
 
    def __init__(self, iterable):
139
 
        self._iterable = iter(iterable)
140
 
        self._buf = None
141
 
 
142
 
    def __iter__(self):
143
 
        return self
144
 
 
145
 
    def put(self, s):
146
 
        self._buf = s
147
 
 
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()
155
 
 
156
 
###
157
 
### SCRIPT HANDLING
158
 
###
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.
165
 
    """
166
 
    def __init__(self, script, *args , **kwargs):
167
 
        """Create a new Script instance.  Instance attributes:
168
 
 
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.
181
 
        """
182
 
        KickstartObject.__init__(self, *args, **kwargs)
183
 
        self.script = "".join(script)
184
 
 
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)
191
 
 
192
 
    def __str__(self):
193
 
        """Return a string formatted for output to a kickstart file."""
194
 
        retval = ""
195
 
 
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'
202
 
 
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"
211
 
 
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
222
 
 
223
 
 
224
 
##
225
 
## PACKAGE HANDLING
226
 
##
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:
231
 
 
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.
235
 
        """
236
 
        self.name = name
237
 
        self.include = include
238
 
 
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
247
 
 
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
254
 
 
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:
259
 
 
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.
288
 
 
289
 
        """
290
 
        KickstartObject.__init__(self, *args, **kwargs)
291
 
 
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
305
 
 
306
 
    def __str__(self):
307
 
        """Return a string formatted for output to a kickstart file."""
308
 
        pkgs = ""
309
 
 
310
 
        if not self.default:
311
 
            if self.environment:
312
 
                pkgs += "@^%s\n" % self.environment
313
 
 
314
 
            grps = self.groupList
315
 
            grps.sort()
316
 
            for grp in grps:
317
 
                pkgs += "%s\n" % grp.__str__()
318
 
 
319
 
            p = self.packageList
320
 
            p.sort()
321
 
            for pkg in p:
322
 
                pkgs += "%s\n" % pkg
323
 
 
324
 
            grps = self.excludedGroupList
325
 
            grps.sort()
326
 
            for grp in grps:
327
 
                pkgs += "-%s\n" % grp.__str__()
328
 
 
329
 
            p = self.excludedList
330
 
            p.sort()
331
 
            for pkg in p:
332
 
                pkgs += "-%s\n" % pkg
333
 
 
334
 
            if pkgs == "":
335
 
                return ""
336
 
 
337
 
        retval = "\n%packages"
338
 
 
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"
353
 
 
354
 
        if ver >= version.F8:
355
 
            return retval + "\n" + pkgs + "\n%end\n"
356
 
        else:
357
 
            return retval + "\n" + pkgs + "\n"
358
 
 
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)
363
 
 
364
 
        (opts, extra) = op.parse_args(args=line.split())
365
 
 
366
 
        if opts.nodefaults and opts.optional:
367
 
            raise KickstartValueError(_("Group cannot specify both --nodefaults and --optional"))
368
 
 
369
 
        # If the group name has spaces in it, we have to put it back together
370
 
        # now.
371
 
        grp = " ".join(extra)
372
 
 
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))
379
 
 
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.
383
 
        """
384
 
        existingExcludedSet = set(self.excludedList)
385
 
        existingPackageSet = set(self.packageList)
386
 
        newExcludedSet = set()
387
 
        newPackageSet = set()
388
 
 
389
 
        excludedGroupList = []
390
 
 
391
 
        for pkg in pkgList:
392
 
            stripped = pkg.strip()
393
 
 
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)
407
 
 
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]
411
 
 
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]
416
 
 
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)
422
 
 
423
 
        existingPackageSet = (existingPackageSet - newExcludedSet) | newPackageSet
424
 
        existingExcludedSet = (existingExcludedSet - existingPackageSet) | newExcludedSet
425
 
 
426
 
        self.packageList = list(existingPackageSet)
427
 
        self.excludedList = list(existingExcludedSet)
428
 
 
429
 
 
430
 
###
431
 
### PARSER
432
 
###
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.
439
 
    """
440
 
    def __init__ (self, handler, followIncludes=True, errorsAreFatal=True,
441
 
                  missingIncludeIsFatal=True):
442
 
        """Create a new KickstartParser instance.  Instance attributes:
443
 
 
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?
457
 
        """
458
 
        self.errorsAreFatal = errorsAreFatal
459
 
        self.followIncludes = followIncludes
460
 
        self.handler = handler
461
 
        self.currentdir = {}
462
 
        self.missingIncludeIsFatal = missingIncludeIsFatal
463
 
 
464
 
        self._state = STATE_COMMANDS
465
 
        self._includeDepth = 0
466
 
        self._line = ""
467
 
 
468
 
        self.version = self.handler.version
469
 
 
470
 
        global ver
471
 
        ver = self.version
472
 
 
473
 
        self._sections = {}
474
 
        self.setupSections()
475
 
 
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
480
 
 
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.
484
 
        """
485
 
        return self._sections[s]
486
 
 
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.
492
 
        """
493
 
        if self.handler:
494
 
            self.handler.currentCmd = args[0]
495
 
            self.handler.currentLine = self._line
496
 
            retval = self.handler.dispatcher(args, lineno)
497
 
 
498
 
            return retval
499
 
 
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.
505
 
        """
506
 
        if not obj.sectionOpen:
507
 
            raise TypeError("no sectionOpen given for section %s" % obj)
508
 
 
509
 
        if not obj.sectionOpen.startswith("%"):
510
 
            raise TypeError("section %s tag does not start with a %%" % obj.sectionOpen)
511
 
 
512
 
        self._sections[obj.sectionOpen] = obj
513
 
 
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.
518
 
        """
519
 
        obj.finalize()
520
 
        self._state = STATE_COMMANDS
521
 
 
522
 
    def _handleSpecialComments(self, line):
523
 
        """Kickstart recognizes a couple special comments."""
524
 
        if self._state != STATE_COMMANDS:
525
 
            return
526
 
 
527
 
        # Save the platform for s-c-kickstart.
528
 
        if line[:10] == "#platform=":
529
 
            self.handler.platform = self._line[11:]
530
 
 
531
 
    def _readSection(self, lineIter, lineno):
532
 
        obj = self._sections[self._state]
533
 
 
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))
541
 
 
542
 
                    self._finalize(obj)
543
 
            except StopIteration:
544
 
                break
545
 
 
546
 
            lineno += 1
547
 
 
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
552
 
 
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
563
 
 
564
 
                args = shlex.split(line)
565
 
 
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))
573
 
 
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))
582
 
 
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)
593
 
 
594
 
        return lineno
595
 
 
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())
599
 
 
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.
604
 
        """
605
 
        try:
606
 
            fn()
607
 
        except Exception, msg:
608
 
            if self.errorsAreFatal:
609
 
                raise
610
 
            else:
611
 
                print(msg)
612
 
 
613
 
    def _isBlankOrComment(self, line):
614
 
        return line.isspace() or line == "" or line.lstrip()[0] == '#'
615
 
 
616
 
    def _handleInclude(self, f):
617
 
        # This case comes up primarily in ksvalidator.
618
 
        if not self.followIncludes:
619
 
            return
620
 
 
621
 
        self._includeDepth += 1
622
 
 
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
631
 
 
632
 
        self._includeDepth -= 1
633
 
 
634
 
    def _stateMachine(self, lineIter):
635
 
        # For error reporting.
636
 
        lineno = 0
637
 
 
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
646
 
 
647
 
            lineno += 1
648
 
 
649
 
            # Eliminate blank lines, whitespace-only lines, and comments.
650
 
            if self._isBlankOrComment(self._line):
651
 
                self._handleSpecialComments(self._line)
652
 
                continue
653
 
 
654
 
            # Split the line, discarding comments.
655
 
            args = shlex.split(self._line, comments=True)
656
 
 
657
 
            if args[0] == "%include":
658
 
                if len(args) == 1 or not args[1]:
659
 
                    raise KickstartParseError(formatErrorMsg(lineno))
660
 
 
661
 
                self._handleInclude(args[1])
662
 
                continue
663
 
 
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)))
675
 
 
676
 
                    self._state = newSection
677
 
                    obj = self._sections[self._state]
678
 
                    self._tryFunc(lambda: obj.handleHeader(lineno, args))
679
 
 
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)
693
 
 
694
 
    def readKickstartFromString (self, s, reset=True):
695
 
        """Process a kickstart file, provided as the string str."""
696
 
        if reset:
697
 
            self._reset()
698
 
 
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)
704
 
 
705
 
    def readKickstart(self, f, reset=True):
706
 
        """Process a kickstart file, given by the filename f."""
707
 
        if reset:
708
 
            self._reset()
709
 
 
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)
719
 
 
720
 
        cd = os.path.dirname(f)
721
 
        if not cd.startswith("/"):
722
 
            cd = os.path.abspath(cd)
723
 
        self.currentdir[self._includeDepth] = cd
724
 
 
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))
729
 
 
730
 
        self.readKickstartFromString(s, reset=False)
731
 
 
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.
736
 
        """
737
 
        self._sections = {}
738
 
 
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))