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