--- 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@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.