Merge release-0.28.17 from 'tools/mic'
[platform/upstream/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 as 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 as 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         elif self.type == constants.KS_SCRIPT_UMOUNT:
207             retval += '\n%post-umount'
208
209         if self.interp != "/bin/sh" and self.interp != "":
210             retval += " --interpreter=%s" % self.interp
211         if self.type == constants.KS_SCRIPT_POST and not self.inChroot:
212             retval += " --nochroot"
213         if self.logfile != None:
214             retval += " --logfile %s" % self.logfile
215         if self.errorOnFail:
216             retval += " --erroronfail"
217
218         if self.script.endswith("\n"):
219             if ver >= version.F8:
220                 return retval + "\n%s%%end\n" % self.script
221             else:
222                 return retval + "\n%s\n" % self.script
223         else:
224             if ver >= version.F8:
225                 return retval + "\n%s\n%%end\n" % self.script
226             else:
227                 return retval + "\n%s\n" % self.script
228
229
230 ##
231 ## PACKAGE HANDLING
232 ##
233 class Group:
234     """A class representing a single group in the %packages section."""
235     def __init__(self, name="", include=constants.GROUP_DEFAULT):
236         """Create a new Group instance.  Instance attributes:
237
238            name    -- The group's identifier
239            include -- The level of how much of the group should be included.
240                       Values can be GROUP_* from pykickstart.constants.
241         """
242         self.name = name
243         self.include = include
244
245     def __str__(self):
246         """Return a string formatted for output to a kickstart file."""
247         if self.include == constants.GROUP_REQUIRED:
248             return "@%s --nodefaults" % self.name
249         elif self.include == constants.GROUP_ALL:
250             return "@%s --optional" % self.name
251         else:
252             return "@%s" % self.name
253
254     def __cmp__(self, other):
255         if self.name < other.name:
256             return -1
257         elif self.name > other.name:
258             return 1
259         return 0
260
261 class Packages(KickstartObject):
262     """A class representing the %packages section of the kickstart file."""
263     def __init__(self, *args, **kwargs):
264         """Create a new Packages instance.  Instance attributes:
265
266            addBase       -- Should the Base group be installed even if it is
267                             not specified?
268            default       -- Should the default package set be selected?
269            excludedList  -- A list of all the packages marked for exclusion in
270                             the %packages section, without the leading minus
271                             symbol.
272            excludeDocs   -- Should documentation in each package be excluded?
273            groupList     -- A list of Group objects representing all the groups
274                             specified in the %packages section.  Names will be
275                             stripped of the leading @ symbol.
276            excludedGroupList -- A list of Group objects representing all the
277                                 groups specified for removal in the %packages
278                                 section.  Names will be stripped of the leading
279                                 -@ symbols.
280            handleMissing -- If unknown packages are specified in the %packages
281                             section, should it be ignored or not?  Values can
282                             be KS_MISSING_* from pykickstart.constants.
283            packageList   -- A list of all the packages specified in the
284                             %packages section.
285            instLangs     -- A list of languages to install.
286         """
287         KickstartObject.__init__(self, *args, **kwargs)
288
289         self.addBase = True
290         self.default = False
291         self.excludedList = []
292         self.excludedGroupList = []
293         self.excludeDocs = False
294         self.groupList = []
295         self.handleMissing = constants.KS_MISSING_PROMPT
296         self.packageList = []
297         self.tpk_packageList = []
298         self.instLangs = None
299
300     def __str__(self):
301         """Return a string formatted for output to a kickstart file."""
302         pkgs = ""
303
304         if not self.default:
305             grps = self.groupList
306             grps.sort()
307             for grp in grps:
308                 pkgs += "%s\n" % grp.__str__()
309
310             p = self.packageList
311             p.sort()
312             for pkg in p:
313                 pkgs += "%s\n" % pkg
314
315             grps = self.excludedGroupList
316             grps.sort()
317             for grp in grps:
318                 pkgs += "-%s\n" % grp.__str__()
319
320             p = self.excludedList
321             p.sort()
322             for pkg in p:
323                 pkgs += "-%s\n" % pkg
324
325             if pkgs == "":
326                 return ""
327
328         retval = "\n%packages"
329
330         if self.default:
331             retval += " --default"
332         if self.excludeDocs:
333             retval += " --excludedocs"
334         if not self.addBase:
335             retval += " --nobase"
336         if self.handleMissing == constants.KS_MISSING_IGNORE:
337             retval += " --ignoremissing"
338         if self.instLangs:
339             retval += " --instLangs=%s" % self.instLangs
340
341         if ver >= version.F8:
342             return retval + "\n" + pkgs + "\n%end\n"
343         else:
344             return retval + "\n" + pkgs + "\n"
345
346     def _processGroup (self, line):
347         op = OptionParser()
348         op.add_option("--nodefaults", action="store_true", default=False)
349         op.add_option("--optional", action="store_true", default=False)
350
351         (opts, extra) = op.parse_args(args=line.split())
352
353         if opts.nodefaults and opts.optional:
354             raise KickstartValueError (_("Group cannot specify both --nodefaults and --optional"))
355
356         # If the group name has spaces in it, we have to put it back together
357         # now.
358         grp = " ".join(extra)
359
360         if opts.nodefaults:
361             self.groupList.append(Group(name=grp, include=constants.GROUP_REQUIRED))
362         elif opts.optional:
363             self.groupList.append(Group(name=grp, include=constants.GROUP_ALL))
364         else:
365             self.groupList.append(Group(name=grp, include=constants.GROUP_DEFAULT))
366
367     def add (self, pkgList):
368         """Given a list of lines from the input file, strip off any leading
369            symbols and add the result to the appropriate list.
370         """
371         existingExcludedSet = set(self.excludedList)
372         existingPackageSet = set(self.packageList)
373         newExcludedSet = set()
374         newPackageSet = set()
375
376         excludedGroupList = []
377
378         for pkg in pkgList:
379             stripped = pkg.strip()
380
381             if stripped[0] == "@":
382                 self._processGroup(stripped[1:])
383             elif stripped[0] == "-":
384                 if stripped[1] == "@":
385                     excludedGroupList.append(Group(name=stripped[2:]))
386                 else:
387                     newExcludedSet.add(stripped[1:])
388             else:
389                 newPackageSet.add(stripped)
390
391         # Groups have to be excluded in two different ways (note: can't use
392         # sets here because we have to store objects):
393         excludedGroupNames = map(lambda g: g.name, excludedGroupList)
394
395         # First, an excluded group may be cancelling out a previously given
396         # one.  This is often the case when using %include.  So there we should
397         # just remove the group from the list.
398         self.groupList = filter(lambda g: g.name not in excludedGroupNames, self.groupList)
399
400         # Second, the package list could have included globs which are not
401         # processed by pykickstart.  In that case we need to preserve a list of
402         # excluded groups so whatever tool doing package/group installation can
403         # take appropriate action.
404         self.excludedGroupList.extend(excludedGroupList)
405
406         existingPackageSet = (existingPackageSet - newExcludedSet) | newPackageSet
407         existingExcludedSet = (existingExcludedSet - existingPackageSet) | newExcludedSet
408
409         self.packageList = list(existingPackageSet)
410         self.excludedList = list(existingExcludedSet)
411
412 class TpkPackages(KickstartObject):
413     """A class representing the %tpk_packages section of the kickstart file."""
414     def __init__(self, *args, **kwargs):
415         KickstartObject.__init__(self, *args, **kwargs)
416         self.tpk_packageList = []
417     def __str__(self):
418         tpk_pkgs = ""
419         retval = "\n%tpk_packages"
420         p = self.packageList
421         p.sort()
422         for pkg in p:
423             tpk_pkgs += "%s\n" % pkg
424         return retval + "\n" +tpk_pkgs
425     def add(self, tpkPackageList):
426         tpk_PackageSet = set(self.tpk_packageList)
427         for tpk_pkg in tpkPackageList:
428             stripped = tpk_pkg.strip()
429             tpk_PackageSet.add(stripped)
430         self.tpk_packageList = list(tpk_PackageSet)
431 ###
432 ### PARSER
433 ###
434 class KickstartParser:
435     """The kickstart file parser class as represented by a basic state
436        machine.  To create a specialized parser, make a subclass and override
437        any of the methods you care about.  Methods that don't need to do
438        anything may just pass.  However, _stateMachine should never be
439        overridden.
440     """
441     def __init__ (self, handler, followIncludes=True, errorsAreFatal=True,
442                   missingIncludeIsFatal=True):
443         """Create a new KickstartParser instance.  Instance attributes:
444
445            errorsAreFatal        -- Should errors cause processing to halt, or
446                                     just print a message to the screen?  This
447                                     is most useful for writing syntax checkers
448                                     that may want to continue after an error is
449                                     encountered.
450            followIncludes        -- If %include is seen, should the included
451                                     file be checked as well or skipped?
452            handler               -- An instance of a BaseHandler subclass.  If
453                                     None, the input file will still be parsed
454                                     but no data will be saved and no commands
455                                     will be executed.
456            missingIncludeIsFatal -- Should missing include files be fatal, even
457                                     if errorsAreFatal is False?
458         """
459         self.errorsAreFatal = errorsAreFatal
460         self.followIncludes = followIncludes
461         self.handler = handler
462         self.currentdir = {}
463         self.missingIncludeIsFatal = missingIncludeIsFatal
464
465         self._state = STATE_COMMANDS
466         self._includeDepth = 0
467         self._line = ""
468
469         self.version = self.handler.version
470
471         global ver
472         ver = self.version
473
474         self._sections = {}
475         self.setupSections()
476
477     def _reset(self):
478         """Reset the internal variables of the state machine for a new kickstart file."""
479         self._state = STATE_COMMANDS
480         self._includeDepth = 0
481
482     def getSection(self, s):
483         """Return a reference to the requested section (s must start with '%'s),
484            or raise KeyError if not found.
485         """
486         return self._sections[s]
487
488     def handleCommand (self, lineno, args):
489         """Given the list of command and arguments, call the Version's
490            dispatcher method to handle the command.  Returns the command or
491            data object returned by the dispatcher.  This method may be
492            overridden in a subclass if necessary.
493         """
494         if self.handler:
495             self.handler.currentCmd = args[0]
496             self.handler.currentLine = self._line
497             retval = self.handler.dispatcher(args, lineno)
498
499             return retval
500
501     def registerSection(self, obj):
502         """Given an instance of a Section subclass, register the new section
503            with the parser.  Calling this method means the parser will
504            recognize your new section and dispatch into the given object to
505            handle it.
506         """
507         if not obj.sectionOpen:
508             raise TypeError ("no sectionOpen given for section %s" % obj)
509
510         if not obj.sectionOpen.startswith("%"):
511             raise TypeError ("section %s tag does not start with a %%" % obj.sectionOpen)
512
513         self._sections[obj.sectionOpen] = obj
514
515     def _finalize(self, obj):
516         """Called at the close of a kickstart section to take any required
517            actions.  Internally, this is used to add scripts once we have the
518            whole body read.
519         """
520         obj.finalize()
521         self._state = STATE_COMMANDS
522
523     def _handleSpecialComments(self, line):
524         """Kickstart recognizes a couple special comments."""
525         if self._state != STATE_COMMANDS:
526             return
527
528         # Save the platform for s-c-kickstart.
529         if line[:10] == "#platform=":
530             self.handler.platform = self._line[11:]
531
532     def _readSection(self, lineIter, lineno):
533         obj = self._sections[self._state]
534
535         while True:
536             try:
537                 line = lineIter.next()
538                 if line == "":
539                     # This section ends at the end of the file.
540                     if self.version >= version.F8:
541                         raise KickstartParseError (formatErrorMsg(lineno, msg=_("Section does not end with %%end.")))
542
543                     self._finalize(obj)
544             except StopIteration:
545                 break
546
547             lineno += 1
548
549             # Throw away blank lines and comments, unless the section wants all
550             # lines.
551             if self._isBlankOrComment(line) and not obj.allLines:
552                 continue
553
554             if line.startswith("%"):
555                 args = shlex.split(line)
556
557                 if args and args[0] == "%end":
558                     # This is a properly terminated section.
559                     self._finalize(obj)
560                     break
561                 elif args and args[0] == "%ksappend":
562                     continue
563                 elif args and (self._validState(args[0]) or args[0] in ["%include", "%ksappend"]):
564                     # This is an unterminated section.
565                     if self.version >= version.F8:
566                         raise KickstartParseError (formatErrorMsg(lineno, msg=_("Section does not end with %%end.")))
567
568                     # Finish up.  We do not process the header here because
569                     # kicking back out to STATE_COMMANDS will ensure that happens.
570                     lineIter.put(line)
571                     lineno -= 1
572                     self._finalize(obj)
573                     break
574             else:
575                 # This is just a line within a section.  Pass it off to whatever
576                 # section handles it.
577                 obj.handleLine(line)
578
579         return lineno
580
581     def _validState(self, st):
582         """Is the given section tag one that has been registered with the parser?"""
583         return st in self._sections.keys()
584
585     def _tryFunc(self, fn):
586         """Call the provided function (which doesn't take any arguments) and
587            do the appropriate error handling.  If errorsAreFatal is False, this
588            function will just print the exception and keep going.
589         """
590         try:
591             fn()
592         except Exception as msg:
593             if self.errorsAreFatal:
594                 raise
595             else:
596                 print (msg)
597
598     def _isBlankOrComment(self, line):
599         return line.isspace() or line == "" or line.lstrip()[0] == '#'
600
601     def _stateMachine(self, lineIter):
602         # For error reporting.
603         lineno = 0
604
605         while True:
606             # Get the next line out of the file, quitting if this is the last line.
607             try:
608                 self._line = lineIter.next()
609                 if self._line == "":
610                     break
611             except StopIteration:
612                 break
613
614             lineno += 1
615
616             # Eliminate blank lines, whitespace-only lines, and comments.
617             if self._isBlankOrComment(self._line):
618                 self._handleSpecialComments(self._line)
619                 continue
620
621             # Remove any end-of-line comments.
622             sanitized = self._line.split("#")[0]
623
624             # Then split the line.
625             args = shlex.split(sanitized.rstrip())
626
627             if args[0] == "%include":
628                 # This case comes up primarily in ksvalidator.
629                 if not self.followIncludes:
630                     continue
631
632                 if len(args) == 1 or not args[1]:
633                     raise KickstartParseError (formatErrorMsg(lineno))
634
635                 self._includeDepth += 1
636
637                 try:
638                     self.readKickstart(args[1], reset=False)
639                 except KickstartError:
640                     # Handle the include file being provided over the
641                     # network in a %pre script.  This case comes up in the
642                     # early parsing in anaconda.
643                     if self.missingIncludeIsFatal:
644                         raise
645
646                 self._includeDepth -= 1
647                 continue
648
649             # Now on to the main event.
650             if self._state == STATE_COMMANDS:
651                 if args[0] == "%ksappend":
652                     # This is handled by the preprocess* functions, so continue.
653                     continue
654                 elif args[0][0] == '%':
655                     # This is the beginning of a new section.  Handle its header
656                     # here.
657                     newSection = args[0]
658                     if not self._validState(newSection):
659                         raise KickstartParseError (formatErrorMsg(lineno, msg=_("Unknown kickstart section: %s" % newSection)))
660
661                     self._state = newSection
662                     obj = self._sections[self._state]
663                     self._tryFunc(lambda: obj.handleHeader(lineno, args))
664
665                     # This will handle all section processing, kicking us back
666                     # out to STATE_COMMANDS at the end with the current line
667                     # being the next section header, etc.
668                     lineno = self._readSection(lineIter, lineno)
669                 else:
670                     # This is a command in the command section.  Dispatch to it.
671                     self._tryFunc(lambda: self.handleCommand(lineno, args))
672             elif self._state == STATE_END:
673                 break
674
675     def readKickstartFromString (self, s, reset=True):
676         """Process a kickstart file, provided as the string str."""
677         if reset:
678             self._reset()
679
680         # Add a "" to the end of the list so the string reader acts like the
681         # file reader and we only get StopIteration when we're after the final
682         # line of input.
683         i = PutBackIterator(s.splitlines(True) + [""])
684         self._stateMachine (i)
685
686     def readKickstart(self, f, reset=True):
687         """Process a kickstart file, given by the filename f."""
688         if reset:
689             self._reset()
690
691         # an %include might not specify a full path.  if we don't try to figure
692         # out what the path should have been, then we're unable to find it
693         # requiring full path specification, though, sucks.  so let's make
694         # the reading "smart" by keeping track of what the path is at each
695         # include depth.
696         if not os.path.exists(f):
697             if self.currentdir.has_key(self._includeDepth - 1):
698                 if os.path.exists(os.path.join(self.currentdir[self._includeDepth - 1], f)):
699                     f = os.path.join(self.currentdir[self._includeDepth - 1], f)
700
701         cd = os.path.dirname(f)
702         if not cd.startswith("/"):
703             cd = os.path.abspath(cd)
704         self.currentdir[self._includeDepth] = cd
705
706         try:
707             s = urlread(f)
708         except grabber.URLGrabError as e:
709             raise KickstartError (formatErrorMsg(0, msg=_("Unable to open input kickstart file: %s") % e.strerror))
710
711         self.readKickstartFromString(s, reset=False)
712
713     def setupSections(self):
714         """Install the sections all kickstart files support.  You may override
715            this method in a subclass, but should avoid doing so unless you know
716            what you're doing.
717         """
718         self._sections = {}
719
720         # Install the sections all kickstart files support.
721         self.registerSection(PreScriptSection(self.handler, dataObj=Script))
722         self.registerSection(PostScriptSection(self.handler, dataObj=Script))
723         self.registerSection(TracebackScriptSection(self.handler, dataObj=Script))
724         self.registerSection(RunScriptSection(self.handler, dataObj=Script))
725         self.registerSection(PostUmountScriptSection(self.handler, dataObj=Script))
726         self.registerSection(PackageSection(self.handler))
727         self.registerSection(TpkPackageSection(self.handler))
728