2 # parser.py: Kickstart file parser.
4 # Chris Lumens <clumens@redhat.com>
6 # Copyright 2005-2016 Red Hat, Inc.
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.
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.
23 Main kickstart file processing module.
25 This module exports several important classes:
27 Script - Representation of a single %pre, %pre-install, %post, or %traceback script.
29 Packages - Representation of the %packages section.
31 KickstartParser - The kickstart file parser state machine.
33 from collections.abc import Iterator
40 from pykickstart import constants, version
41 from pykickstart.errors import KickstartError, KickstartParseError, KickstartParseWarning
42 from pykickstart.ko import KickstartObject
43 from pykickstart.options import KSOptionParser
44 from pykickstart.load import load_to_str
45 from pykickstart.sections import PackageSection, PreScriptSection, PreInstallScriptSection, \
46 PostScriptSection, TracebackScriptSection, OnErrorScriptSection, \
47 NullSection, RunScriptSection, PostUmountScriptSection, TpkPackageSection
49 from pykickstart.i18n import _
52 STATE_COMMANDS = "commands"
54 def _preprocessStateMachine(lineIter):
59 retval = retval.encode(sys.getdefaultencoding())
67 # At the end of the file?
75 if not ll.startswith("%ksappend"):
76 retval += l.encode(sys.getdefaultencoding())
79 # Try to pull down the remote file.
81 ksurl = ll.split(' ')[1]
83 raise KickstartParseError(_("Illegal url for %%ksappend: %s") % ll, lineno=lineno)
86 contents = load_to_str(ksurl)
87 except KickstartError as e:
88 raise KickstartError(_("Unable to open %%ksappend file: %s") % str(e), lineno=lineno)
89 # If that worked, write the remote file to the output kickstart
90 # file in one burst. This allows multiple %ksappend lines to
92 if contents is not None:
93 retval += contents.encode(sys.getdefaultencoding())
97 def preprocessFromStringToString(s):
98 """Preprocess the kickstart file, provided as the string s. This
99 method is currently only useful for handling %ksappend lines, which
100 need to be fetched before the real kickstart parser can be run.
101 Returns the complete kickstart file as a string.
103 i = iter(s.splitlines(True) + [""])
104 return _preprocessStateMachine(i)
106 def preprocessKickstartToString(f):
107 """Preprocess the kickstart file, given by the filename f. This
108 method is currently only useful for handling %ksappend lines,
109 which need to be fetched before the real kickstart parser can be
110 run. Returns the complete kickstart file as a string.
113 contents = load_to_str(f)
114 except KickstartError as e:
115 raise KickstartError(_("Unable to open input kickstart file: %s") % str(e), lineno=0)
117 return _preprocessStateMachine(iter(contents.splitlines(True)))
119 def preprocessFromString(s):
120 """Preprocess the kickstart file, provided as the string s. This
121 method is currently only useful for handling %ksappend lines,
122 which need to be fetched before the real kickstart parser can be
123 run. Returns the location of the complete kickstart file.
125 s = preprocessFromStringToString(s)
128 (outF, outName) = tempfile.mkstemp(suffix="-ks.cfg")
136 def preprocessKickstart(f):
137 """Preprocess the kickstart file, given by the filename f. This
138 method is currently only useful for handling %ksappend lines,
139 which need to be fetched before the real kickstart parser can be
140 run. Returns the location of the complete kickstart file.
142 s = preprocessKickstartToString(f)
145 (outF, outName) = tempfile.mkstemp(suffix="-ks.cfg")
153 class PutBackIterator(Iterator):
154 def __init__(self, iterable):
155 self._iterable = iter(iterable)
170 return next(self._iterable)
173 return self.next() # pylint: disable=not-callable
178 class Script(KickstartObject):
181 """A class representing a single kickstart script. If functionality beyond
182 just a data representation is needed (for example, a run method in
183 anaconda), Script may be subclassed. Although a run method is not
184 provided, most of the attributes of Script have to do with running the
185 script. Instances of Script are held in a list by the Version object.
187 def __init__(self, script, *args, **kwargs):
188 """Create a new Script instance. Instance attributes:
190 :keyword errorOnFail: If execution of the script fails, should anaconda
191 stop, display an error, and then reboot without
192 running any other scripts?
194 :keyword inChroot: Does the script execute in anaconda's chroot
197 :keyword interp: The program that should be used to interpret this
200 :keyword lineno: The line number this script starts on.
202 :keyword logfile: Where all messages from the script should be logged.
204 :keyword script: A string containing all the lines of the script.
206 :keyword type: The type of the script, which can be KS_SCRIPT_* from
207 :mod:`pykickstart.constants`.
209 KickstartObject.__init__(self, *args, **kwargs)
210 self.script = "".join(script)
212 self.interp = kwargs.get("interp", "/bin/sh")
213 self.inChroot = kwargs.get("inChroot", False)
214 self.lineno = kwargs.get("lineno", None)
215 self.logfile = kwargs.get("logfile", None)
216 self.errorOnFail = kwargs.get("errorOnFail", False)
217 self.type = kwargs.get("type", constants.KS_SCRIPT_PRE)
220 """Return a string formatted for output to a kickstart file."""
223 if self.type == constants.KS_SCRIPT_PRE:
225 elif self.type == constants.KS_SCRIPT_POST:
227 elif self.type == constants.KS_SCRIPT_TRACEBACK:
228 retval += '\n%traceback'
229 elif self.type == constants.KS_SCRIPT_PREINSTALL:
230 retval += '\n%pre-install'
231 elif self.type == constants.KS_SCRIPT_ONERROR:
232 retval += '\n%onerror'
233 elif self.type == constants.KS_SCRIPT_RUN:
234 retval += '\n%runscript'
235 elif self.type == constants.KS_SCRIPT_UMOUNT:
236 retval += '\n%post-umount'
238 if self.interp != "/bin/sh" and self.interp:
239 retval += " --interpreter=%s" % self.interp
240 if self.type == constants.KS_SCRIPT_POST and not self.inChroot:
241 retval += " --nochroot"
242 if self.logfile is not None:
243 retval += " --logfile=%s" % self.logfile
245 retval += " --erroronfail"
247 if self.script.endswith("\n"):
248 if self._ver >= version.F8:
249 return retval + "\n%s%%end\n" % self.script
251 return retval + "\n%s" % self.script
253 if self._ver >= version.F8:
254 return retval + "\n%s\n%%end\n" % self.script
256 return retval + "\n%s\n" % self.script
261 class Group(KickstartObject):
262 """A class representing a single group in the %packages section."""
263 def __init__(self, name="", include=constants.GROUP_DEFAULT):
264 """Create a new Group instance. Instance attributes:
266 name -- The group's identifier
267 include -- The level of how much of the group should be included.
268 Values can be GROUP_* from pykickstart.constants.
270 KickstartObject.__init__(self)
272 self.include = include
275 """Return a string formatted for output to a kickstart file."""
276 if self.include == constants.GROUP_REQUIRED:
277 return "@%s --nodefaults" % self.name
278 elif self.include == constants.GROUP_ALL:
279 return "@%s --optional" % self.name
281 return "@%s" % self.name
283 def __lt__(self, other):
284 return self.name < other.name
286 def __le__(self, other):
287 return self.name <= other.name
289 def __eq__(self, other):
290 return self.name == other.name
292 def __ne__(self, other):
293 return self.name != other.name
295 def __gt__(self, other):
296 return self.name > other.name
298 def __ge__(self, other):
299 return self.name >= other.name
301 __hash__ = KickstartObject.__hash__
303 class Packages(KickstartObject):
306 """A class representing the %packages section of the kickstart file."""
307 def __init__(self, *args, **kwargs):
308 """Create a new Packages instance. Instance attributes:
310 addBase -- Should the Base group be installed even if it is
312 nocore -- Should the Core group be skipped? This results in
313 a %packages section that basically only installs the
314 packages you list, and may not be a usable system.
315 default -- Should the default package set be selected?
316 environment -- What base environment should be selected? Only one
317 may be chosen at a time.
318 excludedList -- A list of all the packages marked for exclusion in
319 the %packages section, without the leading minus
321 excludeDocs -- Should documentation in each package be excluded?
322 groupList -- A list of Group objects representing all the groups
323 specified in the %packages section. Names will be
324 stripped of the leading @ symbol.
325 excludedGroupList -- A list of Group objects representing all the
326 groups specified for removal in the %packages
327 section. Names will be stripped of the leading
329 handleMissing -- If unknown packages are specified in the %packages
330 section, should it be ignored or not? Values can
331 be KS_MISSING_* from pykickstart.constants.
332 handleBroken -- If packages with conflicts are specified in the
333 %packages section, should it be ignored or not?
334 Values can be KS_BROKEN_* from pykickstart.constants.
335 packageList -- A list of all the packages specified in the
337 instLangs -- A list of languages to install.
338 multiLib -- Whether to use yum's "all" multilib policy.
339 excludeWeakdeps -- Whether to exclude weak dependencies.
340 timeout -- Number of seconds to wait for a connection before
341 yum's or dnf's timing out or None.
342 retries -- Number of times yum's or dnf's attempt to retrieve
343 a file should retry before returning an error.
344 seen -- If %packages was ever used in the kickstart file,
345 this attribute will be set to True.
348 KickstartObject.__init__(self, *args, **kwargs)
353 self.environment = None
354 self.excludedList = []
355 self.excludedGroupList = []
356 self.excludeDocs = False
358 self.handleMissing = constants.KS_MISSING_PROMPT
359 self.handleBroken = constants.KS_BROKEN_REPORT
360 self.packageList = []
361 self.tpk_packageList = []
362 self.instLangs = None
363 self.multiLib = False
364 self.excludeWeakdeps = False
370 """Return a string formatted for output to a kickstart file."""
371 pkgs = self._processPackagesContent()
375 retval += " --default"
377 retval += " --excludedocs"
379 retval += " --nobase"
381 retval += " --nocore"
382 if self.handleMissing == constants.KS_MISSING_IGNORE:
383 retval += " --ignoremissing"
384 if self.handleBroken == constants.KS_BROKEN_IGNORE:
385 retval += " --ignorebroken"
386 if self.instLangs is not None:
387 retval += " --inst-langs=%s" % self.instLangs
389 retval += " --multilib"
390 if self.excludeWeakdeps:
391 retval += " --exclude-weakdeps"
392 if self.timeout is not None:
393 retval += " --timeout=%d" % self.timeout
394 if self.retries is not None:
395 retval += " --retries=%d" % self.retries
397 if retval == "" and pkgs == "" and not self.seen:
400 if self._ver >= version.F8:
401 return "\n%packages" + retval + "\n" + pkgs + "\n%end\n"
403 return "\n%packages" + retval + "\n" + pkgs + "\n"
405 def _processPackagesContent(self):
410 pkgs += "@^%s\n" % self.environment
412 grps = self.groupList
415 pkgs += "%s\n" % grp.__str__()
422 grps = self.excludedGroupList
425 pkgs += "-%s\n" % grp.__str__()
427 p = self.excludedList
430 pkgs += "-%s\n" % pkg
434 def _processGroup(self, line):
435 op = KSOptionParser(prog="", description="", version=version.DEVEL)
436 op.add_argument("--nodefaults", action="store_true", default=False,
437 help="", version=version.DEVEL)
438 op.add_argument("--optional", action="store_true", default=False,
439 help="", version=version.DEVEL)
441 (ns, extra) = op.parse_known_args(args=line.split())
443 if ns.nodefaults and ns.optional:
444 raise KickstartParseError(_("Group cannot specify both --nodefaults and --optional"))
446 # If the group name has spaces in it, we have to put it back together
448 grp = " ".join(extra)
450 if grp in [g.name for g in self.groupList]:
454 self.groupList.append(Group(name=grp, include=constants.GROUP_REQUIRED))
456 self.groupList.append(Group(name=grp, include=constants.GROUP_ALL))
458 self.groupList.append(Group(name=grp, include=constants.GROUP_DEFAULT))
460 def add(self, pkgList):
461 """Given a list of lines from the input file, strip off any leading
462 symbols and add the result to the appropriate list.
464 existingExcludedSet = set(self.excludedList)
465 existingPackageSet = set(self.packageList)
466 newExcludedSet = set()
467 newPackageSet = set()
469 excludedGroupList = []
472 stripped = pkg.strip()
474 if stripped[0:2] == "@^":
475 self.environment = stripped[2:]
476 elif stripped[0] == "@":
477 self._processGroup(stripped[1:])
478 elif stripped[0] == "-":
479 if stripped[1:3] == "@^" and self.environment == stripped[3:]:
480 self.environment = None
481 elif stripped[1] == "@":
482 excludedGroupList.append(Group(name=stripped[2:]))
484 newExcludedSet.add(stripped[1:])
486 newPackageSet.add(stripped)
488 # Groups have to be excluded in two different ways (note: can't use
489 # sets here because we have to store objects):
490 excludedGroupNames = [g.name for g in excludedGroupList]
492 # First, an excluded group may be cancelling out a previously given
493 # one. This is often the case when using %include. So there we should
494 # just remove the group from the list.
495 self.groupList = [g for g in self.groupList if g.name not in excludedGroupNames]
497 # Second, the package list could have included globs which are not
498 # processed by pykickstart. In that case we need to preserve a list of
499 # excluded groups so whatever tool doing package/group installation can
500 # take appropriate action.
501 self.excludedGroupList.extend(excludedGroupList)
503 existingPackageSet = (existingPackageSet - newExcludedSet) | newPackageSet
504 existingExcludedSet = (existingExcludedSet - existingPackageSet) | newExcludedSet
506 # FIXME: figure these types out
507 self.packageList = sorted(existingPackageSet)
508 self.excludedList = sorted(existingExcludedSet)
510 class TpkPackages(KickstartObject):
511 """A class representing the %tpk_packages section of the kickstart file."""
512 def __init__(self, *args, **kwargs):
513 KickstartObject.__init__(self, *args, **kwargs)
514 self.tpk_packageList = []
517 retval = "\n%tpk_packages"
521 tpk_pkgs += "%s\n" % pkg
522 return retval + "\n" +tpk_pkgs
523 def add(self, tpkPackageList):
524 tpk_PackageSet = set(self.tpk_packageList)
525 for tpk_pkg in tpkPackageList:
526 stripped = tpk_pkg.strip()
527 tpk_PackageSet.add(stripped)
528 self.tpk_packageList = list(tpk_PackageSet)
532 class KickstartParser(object):
533 """The kickstart file parser class as represented by a basic state
534 machine. To create a specialized parser, make a subclass and override
535 any of the methods you care about. Methods that don't need to do
536 anything may just pass. However, _stateMachine should never be
539 def __init__(self, handler, followIncludes=True, errorsAreFatal=True,
540 missingIncludeIsFatal=True, unknownSectionIsFatal=True):
541 """Create a new KickstartParser instance. Instance attributes:
543 errorsAreFatal -- Should errors cause processing to halt, or
544 just print a message to the screen? This
545 is most useful for writing syntax checkers
546 that may want to continue after an error is
548 followIncludes -- If %include is seen, should the included
549 file be checked as well or skipped?
550 handler -- An instance of a BaseHandler subclass. If
551 None, the input file will still be parsed
552 but no data will be saved and no commands
554 missingIncludeIsFatal -- Should missing include files be fatal, even
555 if errorsAreFatal is False?
556 unknownSectionIsFatal -- Should an unknown %section be fatal? Not all
557 sections are handled by pykickstart. Some are
558 user-defined, so there should be a way to have
559 pykickstart ignore them.
561 self.errorsAreFatal = errorsAreFatal
563 self.followIncludes = followIncludes
564 self.handler = handler
566 self.missingIncludeIsFatal = missingIncludeIsFatal
567 self.unknownSectionIsFatal = unknownSectionIsFatal
569 self._state = STATE_COMMANDS
570 self._includeDepth = 0
573 self.version = self.handler.version
574 Script._ver = self.version
575 Packages._ver = self.version
581 """Reset the internal variables of the state machine for a new kickstart file."""
582 self._state = STATE_COMMANDS
583 self._includeDepth = 0
585 def getSection(self, s):
586 """Return a reference to the requested section (s must start with '%'s),
587 or raise KeyError if not found.
589 return self._sections[s]
591 def handleCommand(self, lineno, args):
592 """Given the list of command and arguments, call the Version's
593 dispatcher method to handle the command. Returns the command or
594 data object returned by the dispatcher. This method may be
595 overridden in a subclass if necessary.
598 self.handler.currentLine = self._line
599 retval = self.handler.dispatcher(args, lineno)
602 def registerSection(self, obj):
603 """Given an instance of a Section subclass, register the new section
604 with the parser. Calling this method means the parser will
605 recognize your new section and dispatch into the given object to
608 if not obj.sectionOpen:
609 raise TypeError("no sectionOpen given for section %s" % obj)
611 if not obj.sectionOpen.startswith("%"):
612 raise TypeError("section %s tag does not start with a %%" % obj.sectionOpen)
614 self._sections[obj.sectionOpen] = obj
616 def _finalize(self, obj):
617 """Called at the close of a kickstart section to take any required
618 actions. Internally, this is used to add scripts once we have the
622 self._state = STATE_COMMANDS
624 def _handleSpecialComments(self, line):
625 """Kickstart recognizes a couple special comments."""
626 if self._state != STATE_COMMANDS:
629 # Save the platform for s-c-kickstart.
630 if line[:10] == "#platform=":
631 self.handler.platform = self._line[10:].strip()
633 def _readSection(self, lineIter, lineno):
634 obj = self._sections[self._state]
638 line = next(lineIter)
639 if line == "" and self._includeDepth == 0:
640 # This section ends at the end of the file.
641 if self.version >= version.F8:
642 raise KickstartParseError(_("Section %s does not end with %%end.") % obj.sectionOpen, lineno=lineno)
645 except StopIteration:
650 # Throw away blank lines and comments, unless the section wants all
652 if self._isBlankOrComment(line) and not obj.allLines:
655 if line.lstrip().startswith("%"):
656 # If we're in a script, the line may begin with "%something"
657 # that's not the start of any section we recognize, but still
658 # valid for that script. So, don't do the split below unless
660 possibleSectionStart = line.split()[0]
661 if not self._validState(possibleSectionStart) \
662 and possibleSectionStart not in ("%end", "%include"):
666 args = shlex.split(line)
668 if args and args[0] == "%end":
669 # This is a properly terminated section.
672 elif args and args[0] == "%include":
673 if len(args) == 1 or not args[1]:
674 raise KickstartParseError(lineno=lineno)
676 self._handleInclude(args[1])
678 elif args and args[0] == "%ksappend":
680 elif args and self._validState(args[0]):
681 # This is an unterminated section.
682 if self.version >= version.F8:
683 raise KickstartParseError(_("Section %s does not end with %%end.") % obj.sectionOpen, lineno=lineno)
685 # Finish up. We do not process the header here because
686 # kicking back out to STATE_COMMANDS will ensure that happens.
692 # This is just a line within a section. Pass it off to whatever
693 # section handles it.
698 def _validState(self, st):
699 """Is the given section tag one that has been registered with the parser?"""
700 return st in list(self._sections.keys())
702 def _tryFunc(self, fn):
703 """Call the provided function (which doesn't take any arguments) and
704 do the appropriate error handling. If errorsAreFatal is False, this
705 function will just print the exception and keep going.
709 except Exception as msg: # pylint: disable=broad-except
710 self.errorsCount += 1
711 if self.errorsAreFatal:
714 print(msg, file=sys.stderr)
716 def _isBlankOrComment(self, line):
717 return line.isspace() or line == "" or line.lstrip()[0] == '#'
719 def _handleInclude(self, f):
720 # This case comes up primarily in ksvalidator.
721 if not self.followIncludes:
724 self._includeDepth += 1
727 self.readKickstart(f, reset=False)
728 except KickstartError:
729 # Handle the include file being provided over the
730 # network in a %pre script. This case comes up in the
731 # early parsing in anaconda.
732 if self.missingIncludeIsFatal:
735 self._includeDepth -= 1
737 def _stateMachine(self, lineIter):
738 # For error reporting.
742 # Get the next line out of the file, quitting if this is the last line.
744 self._line = next(lineIter)
747 except StopIteration:
752 # Eliminate blank lines, whitespace-only lines, and comments.
753 if self._isBlankOrComment(self._line):
754 self._handleSpecialComments(self._line)
757 # Split the line, discarding comments.
758 args = shlex.split(self._line, comments=True)
760 if args[0] == "%include":
761 if len(args) == 1 or not args[1]:
762 raise KickstartParseError(lineno=lineno)
764 self._handleInclude(args[1])
767 # Now on to the main event.
768 if self._state == STATE_COMMANDS:
769 if args[0] == "%ksappend":
770 # This is handled by the preprocess* functions, so continue.
772 elif args[0][0] == '%':
773 # This is the beginning of a new section. Handle its header
776 if not self._validState(newSection):
777 if self.unknownSectionIsFatal:
778 raise KickstartParseError(_("Unknown kickstart section: %s") % newSection, lineno=lineno)
780 # If we are ignoring unknown section errors, just create a new
781 # NullSection for the header we just saw. Then nothing else
782 # needs to change. You can turn this warning into an error via
783 # ksvalidator, or the warnings module.
784 warnings.warn(_("Potentially unknown section seen at line %(lineno)s: %(sectionName)s") % {"lineno": lineno, "sectionName": newSection}, KickstartParseWarning)
785 self.registerSection(NullSection(self.handler, sectionOpen=newSection))
787 self._state = newSection
788 obj = self._sections[self._state]
789 self._tryFunc(lambda: obj.handleHeader(lineno, args))
791 # This will handle all section processing, kicking us back
792 # out to STATE_COMMANDS at the end with the current line
793 # being the next section header, etc.
794 lineno = self._readSection(lineIter, lineno)
796 # This is a command in the command section. Dispatch to it.
797 self._tryFunc(lambda: self.handleCommand(lineno, args))
798 elif self._state == STATE_END:
800 elif self._includeDepth > 0:
801 lineIter.put(self._line)
803 lineno = self._readSection(lineIter, lineno)
805 def readKickstartFromString(self, s, reset=True):
806 """Process a kickstart file, provided as the string str."""
810 # Add a "" to the end of the list so the string reader acts like the
811 # file reader and we only get StopIteration when we're after the final
813 i = PutBackIterator(s.splitlines(True) + [""])
814 self._stateMachine(i)
816 def readKickstart(self, f, reset=True):
817 """Process a kickstart file, given by the filename f."""
821 # an %include might not specify a full path. if we don't try to figure
822 # out what the path should have been, then we're unable to find it
823 # requiring full path specification, though, sucks. so let's make
824 # the reading "smart" by keeping track of what the path is at each
826 if not os.path.exists(f):
827 if self._includeDepth - 1 in self.currentdir:
828 if os.path.exists(os.path.join(self.currentdir[self._includeDepth - 1], f)):
829 f = os.path.join(self.currentdir[self._includeDepth - 1], f)
831 cd = os.path.dirname(f)
832 if not cd.startswith("/"):
833 cd = os.path.abspath(cd)
834 self.currentdir[self._includeDepth] = cd
838 except KickstartError as e:
839 raise KickstartError(_("Unable to open input kickstart file: %s") % str(e), lineno=0)
841 self.readKickstartFromString(s, reset=False)
843 def setupSections(self):
844 """Install the sections all kickstart files support. You may override
845 this method in a subclass, but should avoid doing so unless you know
850 # Install the sections all kickstart files support.
851 self.registerSection(PreScriptSection(self.handler, dataObj=Script))
852 self.registerSection(PreInstallScriptSection(self.handler, dataObj=Script))
853 self.registerSection(PostScriptSection(self.handler, dataObj=Script))
854 self.registerSection(OnErrorScriptSection(self.handler, dataObj=Script))
855 self.registerSection(TracebackScriptSection(self.handler, dataObj=Script))
856 self.registerSection(RunScriptSection(self.handler, dataObj=Script))
857 self.registerSection(PostUmountScriptSection(self.handler, dataObj=Script))
858 self.registerSection(PackageSection(self.handler))
859 self.registerSection(TpkPackageSection(self.handler))
861 # Whitelist well-known sections that pykickstart does not understand,
862 # but shouldn't error on.
863 self.registerSection(NullSection(self.handler, sectionOpen="%addon"))
864 self.registerSection(NullSection(self.handler, sectionOpen="%anaconda"))