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