[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