[PATCH 15/16] Add support for KS %addon section and the API+code to use it
Vratislav Podzimek
vpodzime at redhat.com
Thu Dec 13 16:33:26 UTC 2012
On Thu, 2012-12-06 at 16:46 +0100, Martin Sivak wrote:
> ---
> anaconda | 3 +-
> pyanaconda/addons.py | 142 ++++++++++++++++++++++++++++++++++++++++++++++
> pyanaconda/constants.py | 5 +-
> pyanaconda/install.py | 5 +-
> pyanaconda/kickstart.py | 41 +++++++++++--
> pyanaconda/ui/__init__.py | 8 +++
> 6 files changed, 197 insertions(+), 7 deletions(-)
> create mode 100644 pyanaconda/addons.py
>
> diff --git a/anaconda b/anaconda
> index 0789094..01de6f8 100755
> --- a/anaconda
> +++ b/anaconda
> @@ -32,6 +32,7 @@
>
> import atexit, sys, os, re, time, subprocess
> from tempfile import mkstemp
> +from pyanaconda import constants
>
> # keep up with process ID of the window manager if we start it
> wm_pid = None
> @@ -828,7 +829,7 @@ if __name__ == "__main__":
> break
>
> if not ksdata:
> - ksdata = kickstart.AnacondaKSHandler()
> + ksdata = kickstart.AnacondaKSHandler(constants.addon_paths)
>
> if ksdata.rescue.rescue:
> anaconda.rescue = True
> diff --git a/pyanaconda/addons.py b/pyanaconda/addons.py
> new file mode 100644
> index 0000000..0220107
> --- /dev/null
> +++ b/pyanaconda/addons.py
> @@ -0,0 +1,142 @@
> +# Methods and API for anaconda/firstboot 3rd party addons
> +#
> +# Copyright (C) 2012 Red Hat, Inc.
> +#
> +# This copyrighted material is made available to anyone wishing to use,
> +# modify, copy, or redistribute it subject to the terms and conditions of
> +# the GNU General Public License v.2, or (at your option) any later version.
> +# This program is distributed in the hope that it will be useful, but WITHOUT
> +# ANY WARRANTY expressed or implied, including the implied warranties of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
> +# Public License for more details. You should have received a copy of the
> +# GNU General Public License along with this program; if not, write to the
> +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
> +# source code or documentation are not subject to the GNU General Public
> +# License and may only be used or replicated with the express permission of
> +# Red Hat, Inc.
> +#
> +# Red Hat Author(s): Martin Sivak <msivak at redhat.com>
> +#
> +
> +__all__ = ["AddonSection", "AddonRegistry", "AddonData", "collect_addon_paths"]
> +
> +import os
> +from pykickstart.sections import Section
> +
> +def collect_addon_paths(toplevel_addon_paths, ui_subdir = "gui"):
no spaces needed^^^^^^^
> + """This method looks into the directories present
> + in toplevel_addon_paths and registers each subdirectory
> + as a new addonidentified by that subdirectory name.
^^^ space missing
> +
> + It then registers spokes, categories and data (ks)
> + paths for the application to use. By default is looks
> + for spokes and categories in <addon>/gui/ subdirectory
> + but that can be changed using the ui_subdir argument."""
> +
> + module_paths = {
> + "spokes": [],
> + "ks": [],
> + "categories": []
> + }
> +
> + for path in toplevel_addon_paths:
> + try:
> + files = os.listdir(path)
Maybe 'directories' instead of 'files' would be better here? We expect
directories there, right?
> + except OSError:
> + files = []
> +
> + for addon_id in files:
> + addon_ks_path = os.path.join(path, addon_id, "ks")
> + if os.path.isdir(addon_ks_path):
> + module_paths["ks"].append(("anaconda.addon.%s.ks.%%s" % addon_id, addon_ks_path))
> +
> + addon_spoke_path = os.path.join(path, addon_id, ui_subdir, "spokes")
> + if os.path.isdir(addon_spoke_path):
> + module_paths["spokes"].append(("anaconda.addon.%s.spokes.%%s" % addon_id, addon_spoke_path))
> +
> + addon_category_path = os.path.join(path, addon_id, ui_subdir, "categories")
> + if os.path.isdir(addon_spoke_path):
> + module_paths["categories"].append(("anaconda.addon.%s.categories.%%s" % addon_id, addon_category_path))
> +
> + return module_paths
> +
> +class AddonRegistry(object):
> + """This class represents the ksdata.addons object and
> + maintains the ids and data structures for loaded
> + addons.
> +
> + It acts as a proxy during kickstart save.
> + """
> +
> + def __init__(self, dictionary):
> + self.__dict__ = dictionary
> +
> + def __str__(self):
> + return reduce(lambda acc,(id, addon): acc + "%%addon %s\n%s%%end\n" % (id, str(addon)),
> + self.__dict__.iteritems(), "")
> +
> + def execute(self, storage, ksdata, instClass):
> + """This method calls execute on all the registered addons."""
> + for k, v in self.__dict__.iteritems():
> + v.execute(storage, ksdata, instClass)
> +
> +class AddonData(object):
> + """This is a common parent class for loading and storing
> + 3rd party data to kickstart. It is instantiated by
> + kickstart parser and stored as ksdata.addons.<name>
> + to be used in the user interfaces.
> +
> + The mandatory method handle_line receives all lines
> + from the corresponding addon section in kickstart and
> + the mandatory __str__ implementation is responsible for
> + returning the proper kickstart text (to be placed into
> + the %addon section) back.
> +
> + There is also a mandatory method execute, which should
> + make all the described changes to the installed system.
> + """
> +
> + def __init__(self, name):
> + self.name = name
> + self.content = ""
> +
> + def __str__(self):
> + return self.content
> +
> + def execute(self, storage, ksdata, instClass):
> + """Make the changes to the underlying system."""
> + pass
> +
> + def handle_line(self, line):
> + """Process one kickstart line."""
> + self.content += line
Does line end with the newline character?
> +
> +class AddonSection(Section):
> + sectionOpen = "%addon"
> +
> + def __init__(self, *args, **kwargs):
> + Section.__init__(self, *args, **kwargs)
> + self.addon_id = None
> +
> + def handleLine(self, line):
> + if not self.handler:
> + return
> +
> + if not self.addon_id:
> + return
> +
> + addon = getattr(self.handler.addon, self.addon_id)
> + addon.handle_line(line)
> +
> + def handleHeader(self, lineno, args):
> + """Process the arguments to the %addon header."""
> + Section.handleHeader(self, lineno, args)
> + op = KSOptionParser(version=self.version)
> + (opts, extra) = op.parse_args(args=args[1:], lineno=lineno)
> + self.addon_id = extra[0]
> +
> + # if the addon is not registered, create dummy placeholder for it
> + if self.addon_id and not hasattr(self.handler.addon, self.addon_id):
> + setattr(self.handler.addon, self.addon_id, AnacondaKSAddon(self.addon_id))
Why are you using setattr here?
> +
> diff --git a/pyanaconda/constants.py b/pyanaconda/constants.py
> index 7ff5020..5cc066c 100644
> --- a/pyanaconda/constants.py
> +++ b/pyanaconda/constants.py
> @@ -38,6 +38,9 @@ MAX_PART_SIZE = 1024*1024*1024
> # install key related constants
> SKIP_KEY = -50
>
> +# where to look for 3rd party addons
> +addon_paths = ["/usr/share/anaconda/addons"]
Could you change this to ADDON_PATHS?
> +
> # pull in kickstart constants as well
> from pykickstart.constants import *
>
> @@ -96,4 +99,4 @@ USEVNC = _("Start VNC")
> USETEXT = _("Use text mode")
>
> # Runlevel files
> -RUNLEVELS = {3: 'multi-user.target', 5: 'graphical.target'}
> \ No newline at end of file
> +RUNLEVELS = {3: 'multi-user.target', 5: 'graphical.target'}
> diff --git a/pyanaconda/install.py b/pyanaconda/install.py
> index 6754a0a..4e552ea 100644
> --- a/pyanaconda/install.py
> +++ b/pyanaconda/install.py
> @@ -52,7 +52,7 @@ def doConfiguration(storage, payload, ksdata, instClass):
> from pyanaconda import progress
> from pyanaconda.kickstart import runPostScripts
>
> - progress.send_init(4)
> + progress.send_init(5)
>
> # Now run the execute methods of ksdata that require an installed system
> # to be present first.
> @@ -79,6 +79,9 @@ def doConfiguration(storage, payload, ksdata, instClass):
> ksdata.group.execute(storage, ksdata, instClass, u)
> ksdata.user.execute(storage, ksdata, instClass, u)
>
> + with progress_report(_("Configuring addons")):
^^^^ I guess we need a better word here.
> + ksdata.addon.execute(storage, ksdata, instClass, u)
> +
> with progress_report(_("Running post install scripts")):
> runPostScripts(ksdata.scripts)
>
> diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py
> index 1ec294a..693ecd4 100644
> --- a/pyanaconda/kickstart.py
> +++ b/pyanaconda/kickstart.py
> @@ -52,6 +52,8 @@ from pyanaconda import network
> from pyanaconda.simpleconfig import SimpleConfigFile
> from pyanaconda.users import getPassAlgo
> from pyanaconda.desktop import Desktop
> +from .ui.common import collect
> +from .addons import AddonSection, AddonData, AddonRegistry
>
> from pykickstart.base import KickstartCommand
> from pykickstart.constants import *
> @@ -78,6 +80,7 @@ packagesSeen = False
> # so it needs to know about them in some additional way: have the topology ready.
> topology = None
>
> +
No newline needed here and the same goes for the AnacondaKSHandler
class.
> class AnacondaKSScript(KSScript):
> def run(self, chroot):
> if self.inChroot:
> @@ -1342,11 +1345,38 @@ dataMap = {
>
> superclass = returnClassForVersion()
>
> +
> class AnacondaKSHandler(superclass):
> - def __init__ (self):
> + AddonClassType = AddonData
^^^ why CamelCase here?
> +
> + def __init__ (self, addon_paths = []):
> superclass.__init__(self, commandUpdates=commandMap, dataUpdates=dataMap)
> self.onPart = {}
>
> + # collect all kickstart addons for anaconda to addons dictionary
> + # which maps addon_id to it's own data structure based on BaseData
> + # with execute method
> + addons = {}
> +
> + # collect all AddonData subclasses from
> + # for p in addon_paths: <p>/<plugin id>/ks/*.(py|so)
> + # and register them under <plugin id> name
> + for module_name, path in addon_paths:
> + addon_id = os.path.basename(os.path.dirname(os.path.abspath(path)))
Are all os.path.* calls needed here? Why to get abspath when you use
dirname and basename afterwards?
> + if not os.path.isdir(path):
> + continue
> +
> + classes = collect(module_name, path, lambda cls: issubclass(cls, self.AddonClassType))
> + print classes
I guess we don't need this print.
> + if classes:
> + addons[addon_id] = classes[0](name = addon_id)
Could there be multiple classes? And if yes, how it is ensured that the
right one is the first in the list?
> +
> + # Prepare the final structures for 3rd party addons
> + self.addon = AddonRegistry(addons)
> +
> + def __str__(self):
> + return superclass.__str__(self) + "\n" + str(self.addon)
> +
> class AnacondaPreParser(KickstartParser):
> # A subclass of KickstartParser that only looks for %pre scripts and
> # sets them up to be run. All other scripts and commands are ignored.
> @@ -1362,7 +1392,9 @@ class AnacondaPreParser(KickstartParser):
> self.registerSection(NullSection(self.handler, sectionOpen="%post"))
> self.registerSection(NullSection(self.handler, sectionOpen="%traceback"))
> self.registerSection(NullSection(self.handler, sectionOpen="%packages"))
> -
> + self.registerSection(NullSection(self.handler, sectionOpen="%addon"))
> +
> +
> class AnacondaKSParser(KickstartParser):
> def __init__ (self, handler, followIncludes=True, errorsAreFatal=True,
> missingIncludeIsFatal=True, scriptClass=AnacondaKSScript):
> @@ -1380,7 +1412,8 @@ class AnacondaKSParser(KickstartParser):
> self.registerSection(PostScriptSection(self.handler, dataObj=self.scriptClass))
> self.registerSection(TracebackScriptSection(self.handler, dataObj=self.scriptClass))
> self.registerSection(PackageSection(self.handler))
> -
> + self.registerSection(AddonSection(self.handler))
> +
> def preScriptPass(f):
> # The first pass through kickstart file processing - look for %pre scripts
> # and run them. This must come in a separate pass in case a script
> @@ -1399,7 +1432,7 @@ def preScriptPass(f):
> def parseKickstart(f):
> # preprocessing the kickstart file has already been handled in initramfs.
>
> - handler = AnacondaKSHandler()
> + handler = AnacondaKSHandler(constants.addon_paths)
> ksparser = AnacondaKSParser(handler)
>
> # We need this so all the /dev/disk/* stuff is set up before parsing.
> diff --git a/pyanaconda/ui/__init__.py b/pyanaconda/ui/__init__.py
> index 3f39a7a..9337a2a 100644
> --- a/pyanaconda/ui/__init__.py
> +++ b/pyanaconda/ui/__init__.py
> @@ -63,6 +63,14 @@ class UserInterface(object):
> basepath = os.path.dirname(__file__)
> basemask = "pyanaconda.ui"
> paths = PathDict({})
> +
> + @classmethod
> + def update_paths(cls, pathdict):
> + """Receives pathdict and appends it's contents to the current
> + class defined search path dictionary."""
> + for k,v in pathdict.iteritems():
> + cls.paths.setdefault(k, [])
> + cls.paths[k].extend(v)
>
> def setup(self, data):
> """Construct all the objects required to implement this interface.
--
Vratislav Podzimek
Anaconda Rider | Red Hat, Inc. | Brno - Czech Republic
More information about the anaconda-patches
mailing list