[PATCH 15/16] Add support for KS %addon section and the API+code to use it

Martin Sivak msivak at redhat.com
Thu Dec 6 15:46:38 UTC 2012


---
 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"):
+    """This method looks into the directories present
+       in toplevel_addon_paths and registers each subdirectory
+       as a new addonidentified by that subdirectory name.
+
+       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)
+        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
+
+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))
+
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"]
+
 # 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")):
+        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
 
+
 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
+    
+    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)))
+            if not os.path.isdir(path):
+                continue
+
+            classes = collect(module_name, path, lambda cls: issubclass(cls, self.AddonClassType))
+            print classes
+            if classes:
+                addons[addon_id] = classes[0](name = addon_id)
+        
+        # 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.
-- 
1.7.11.7



More information about the anaconda-patches mailing list