--- anaconda.spec.in | 2 +- pyanaconda/addons.py | 120 ------------------------------------------------ pyanaconda/kickstart.py | 32 ++----------- 3 files changed, 5 insertions(+), 149 deletions(-)
diff --git a/anaconda.spec.in b/anaconda.spec.in index 6e4bb25..e4f7e15 100644 --- a/anaconda.spec.in +++ b/anaconda.spec.in @@ -21,7 +21,7 @@ Source0: %{name}-%{version}.tar.bz2 %define gconfversion 2.28.1 %define intltoolver 0.31.2-3 %define libnlver 1.0 -%define pykickstartver 1.99.26 +%define pykickstartver 1.99.27 %define yumver 3.4.3-32 %define partedver 1.8.1 %define pypartedver 2.5-2 diff --git a/pyanaconda/addons.py b/pyanaconda/addons.py index 96eb813..2568d7b 100644 --- a/pyanaconda/addons.py +++ b/pyanaconda/addons.py @@ -18,12 +18,7 @@ # # Red Hat Author(s): Martin Sivak msivak@redhat.com # - -__all__ = ["AddonSection", "AddonRegistry", "AddonData", "collect_addon_paths"] - import os -from pykickstart.sections import Section -from pykickstart.options import KSOptionParser
def collect_addon_paths(toplevel_addon_paths, ui_subdir="gui"): """This method looks into the directories present @@ -61,118 +56,3 @@ def collect_addon_paths(toplevel_addon_paths, ui_subdir="gui"): module_paths["categories"].append(("%s.%s.categories.%%s" % (addon_id, ui_subdir), 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 + str(addon), - self.__dict__.iteritems(), "") - - # pylint: disable-msg=C0103 - def execute(self, storage, ksdata, instClass, users): - """This method calls execute on all the registered addons.""" - for k, v in self.__dict__.iteritems(): - if hasattr(v, "execute"): - v.execute(storage, ksdata, instClass, users) - - def setup(self, storage, ksdata, instClass): - """This method calls setup on all the registered addons.""" - for k, v in self.__dict__.iteritems(): - if hasattr(v, "setup"): - v.setup(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 "%%addon %s\n%s%%end\n" % (self.name, self.content) - - # pylint: disable-msg=C0103 - def setup(self, storage, ksdata, instClass): - """Make the changes to the install system. - - This method is called before the installation - is started and directly from spokes. It must be possible - to call it multiple times without breaking the environment.""" - pass - - def execute(self, storage, ksdata, instClass, users): - """Make the changes to the underlying system. - - This method is called only once in the post-install - setup phase. - """ - pass - - def handle_line(self, line): - """Process one kickstart line.""" - self.content += line - - def finalize(self): - """No additional data will come. - - Addon should check if all mandatory attributes were populated. - """ - pass - -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.addons, 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.addons, self.addon_id): - setattr(self.handler.addons, self.addon_id, AddonData(self.addon_id)) - - def finalize(self): - """Let addon know no additional data will come.""" - Section.finalize() - - addon = getattr(self.handler.addons, self.addon_id) - addon.finalize() diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py index 9060c58..b685bf6 100644 --- a/pyanaconda/kickstart.py +++ b/pyanaconda/kickstart.py @@ -56,7 +56,8 @@ 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, collect_addon_paths +from .addons import collect_addon_paths +from pykickstart.addons import AddonSection
from pykickstart.base import KickstartCommand from pykickstart.constants import * @@ -1422,35 +1423,10 @@ dataMap = { superclass = returnClassForVersion()
class AnacondaKSHandler(superclass): - AddonClassType = AddonData - - def __init__ (self, addon_paths = [], commandUpdates=commandMap, dataUpdates=dataMap): - superclass.__init__(self, commandUpdates=commandUpdates, dataUpdates=dataUpdates) + def __init__ (self, addonPaths = [], commandUpdates=commandMap, dataUpdates=dataMap): + superclass.__init__(self, addonPaths = addonPaths, commandUpdates=commandUpdates, dataUpdates=dataUpdates) 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)) - if classes: - addons[addon_id] = classes[0](name = addon_id) - - # Prepare the structure to track configured spokes - self.configured_spokes = SpokeRegistry() - - # Prepare the final structures for 3rd party addons - self.addons = AddonRegistry(addons) - def __str__(self): return superclass.__str__(self) + "\n" + str(self.addons)
--- pykickstart/addons.py | 308 ++++++++++++++++++++++++++++++++++++++++++++++++++ pykickstart/base.py | 51 ++++++++- pykickstart/parser.py | 2 + 3 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 pykickstart/addons.py
diff --git a/pykickstart/addons.py b/pykickstart/addons.py new file mode 100644 index 0000000..3c6ad1b --- /dev/null +++ b/pykickstart/addons.py @@ -0,0 +1,308 @@ +# 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", "collect_addon_paths"] + +import os +import sys +import types +import imp +import inspect + +from sections import Section +from options import KSOptionParser + +def collect(module_pattern, path, pred): + """Traverse the directory (given by path), import all files as a module + module_pattern % filename and find all classes within that match + the given predicate. This is then returned as a list of classes. + + It is suggested you use collect_categories or collect_spokes instead of + this lower-level method. + + :param module_pattern: the full name pattern (pyanaconda.ui.gui.spokes.%s) + we want to assign to imported modules + :type module_pattern: string + + :param path: the directory we are picking up modules from + :type path: string + + :param pred: function which marks classes as good to import + :type pred: function with one argument returning True or False + """ + + retval = [] + try: + contents = os.listdir(path) + # when the directory "path" does not exist + except OSError: + return [] + + for module_file in contents: + if (not module_file.endswith(".py")) and \ + (not module_file.endswith(".so")): + continue + + if module_file == "__init__.py": + continue + + try: + mod_name = module_file[:module_file.rindex(".")] + except ValueError: + mod_name = module_file + + mod_info = None + module = None + + try: + imp.acquire_lock() + (fo, module_path, module_flags) = imp.find_module(mod_name, [path]) + module = sys.modules.get(module_pattern % mod_name) + + # do not load module if any module with the same name + # is already imported + if not module: + # try importing the module the standard way first + # uses sys.path and the module's full name! + try: + __import__(module_pattern % mod_name) + module = sys.modules[module_pattern % mod_name] + + # if it fails (package-less addon?) try importing single file + # and filling up the package structure voids + except ImportError: + # prepare dummy modules to prevent RuntimeWarnings + module_parts = (module_pattern % mod_name).split(".") + + # remove the last name as it will be inserted by the import + module_parts.pop() + + # make sure all "parent" modules are in sys.modules + for l in range(len(module_parts)): + module_part_name = ".".join(module_parts[:l+1]) + if module_part_name not in sys.modules: + module_part = types.ModuleType(module_part_name) + module_part.__path__ = [path] + sys.modules[module_part_name] = module_part + + # load the collected module + module = imp.load_module(module_pattern % mod_name, + fo, module_path, module_flags) + + # get the filenames without the extensions so we can compare those + # with the .py[co]? equivalence in mind + # - we do not have to care about files without extension as the + # condition at the beginning of the for loop filters out those + # - module_flags[0] contains the extension of the module imp found + candidate_name = module_path[:module_path.rindex(module_flags[0])] + loaded_name, loaded_ext = module.__file__.rsplit(".", 1) + + # restore the extension dot eaten by split + loaded_ext = "." + loaded_ext + + # do not collect classes when the module is already imported + # from different path than we are traversing + # this condition checks the module name without file extension + if candidate_name != loaded_name: + continue + + # if the candidate file is .py[co]? and the loaded is not (.so) + # skip the file as well + if module_flags[0].startswith(".py") and not loaded_ext.startswith(".py"): + continue + + # if the candidate file is not .py[co]? and the loaded is + # skip the file as well + if not module_flags[0].startswith(".py") and loaded_ext.startswith(".py"): + continue + + except ImportError as imperr: + print "Failed to import module in collect: %s" % imperr + continue + finally: + imp.release_lock() + + if mod_info and mod_info[0]: + mod_info[0].close() + + p = lambda obj: inspect.isclass(obj) and pred(obj) + + # if __all__ is defined in the module, use it + if not hasattr(module, "__all__"): + members = inspect.getmembers(module, p) + else: + members = [(name, getattr(module, name)) + for name in module.__all__ + if p(getattr(module, name))] + + for (name, val) in members: + retval.append(val) + + return retval + +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 + str(addon), + self.__dict__.iteritems(), "") + + # pylint: disable-msg=C0103 + def execute(self, storage, ksdata, instClass, users): + """This method calls execute on all the registered addons.""" + for k, v in self.__dict__.iteritems(): + if hasattr(v, "execute"): + v.execute(storage, ksdata, instClass, users) + + def setup(self, storage, ksdata, instClass): + """This method calls setup on all the registered addons.""" + for k, v in self.__dict__.iteritems(): + if hasattr(v, "setup"): + v.setup(storage, ksdata, instClass) + +class AddonCommand(object): + """This class represents a standalone command that belongs + to one AddonData parent. + """ + + writePriority = None + + def __init__(self, commandName, data): + self.data = data + self.commandName = commandName + self.currentCmd = None + self.currentLine = None + self.lineno = None + + def parse(self, args): + """This is called for every line which specifies + the registered command. + + :param args: list of command arguments without + the command name + :type args: list of strings + """ + return self.data + + def __str__(self): + return "" + + def dataList(self): + """This method is required by KS API.""" + return None + + +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. + """ + + COMMAND_MAP = {} + + def __init__(self, name): + self.name = name + self.content = "" + self.seen = False + + def __str__(self): + return "%%addon %s\n%s%%end\n" % (self.name, self.content) + + # pylint: disable-msg=C0103 + def setup(self, storage, ksdata, instClass): + """Make the changes to the install system. + + This method is called before the installation + is started and directly from spokes. It must be possible + to call it multiple times without breaking the environment.""" + pass + + def execute(self, storage, ksdata, instClass, users): + """Make the changes to the underlying system. + + This method is called only once in the post-install + setup phase. + """ + pass + + def handle_line(self, line): + """Process one kickstart line.""" + self.content += line + + def finalize(self): + """No additional data will come. + + Addon should check if all mandatory attributes were populated. + """ + pass + +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.addons, 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.addons, self.addon_id): + setattr(self.handler.addons, self.addon_id, AddonData(self.addon_id)) + + def finalize(self): + """Let addon know no additional data will come.""" + Section.finalize(self) + + addon = getattr(self.handler.addons, self.addon_id) + addon.finalize() diff --git a/pykickstart/base.py b/pykickstart/base.py index 780ebed..f9cc161 100644 --- a/pykickstart/base.py +++ b/pykickstart/base.py @@ -38,6 +38,7 @@ This module exports several important base classes: Command objects are contained within a BaseHandler object. """ +import os import gettext gettext.textdomain("pykickstart") _ = lambda x: gettext.ldgettext("pykickstart", x) @@ -48,6 +49,7 @@ from pykickstart.errors import * from pykickstart.ko import * from pykickstart.parser import Packages from pykickstart.version import versionToString +from pykickstart.addons import AddonData, AddonRegistry, collect
### ### COMMANDS @@ -207,7 +209,7 @@ class BaseHandler(KickstartObject): version = None
def __init__(self, mapping=None, dataMapping=None, commandUpdates=None, - dataUpdates=None, *args, **kwargs): + dataUpdates=None, addonPaths=[], *args, **kwargs): """Create a new BaseHandler instance. This method must be provided by all subclasses, but subclasses must call BaseHandler.__init__ first.
@@ -227,6 +229,8 @@ class BaseHandler(KickstartObject): modify. dataUpdates -- This is the same as commandUpdates, but for data objects. + addonPaths -- List of directories that may contain kickstart + addons
Instance attributes: @@ -259,6 +263,7 @@ class BaseHandler(KickstartObject): self.scripts = [] self.packages = Packages() self.platform = "" + self.AddonData = AddonData
# These will be set by the dispatcher. self.commands = {} @@ -269,9 +274,11 @@ class BaseHandler(KickstartObject): # registerCommand and used in __str__. No one else should be touching # it. self._writeOrder = {} - self._registerCommands(mapping, dataMapping, commandUpdates, dataUpdates)
+ # find all addons and register addon sections + self._processAddons(addonPaths) + def __str__(self): """Return a string formatted for output to a kickstart file.""" retval = "" @@ -301,6 +308,46 @@ class BaseHandler(KickstartObject):
return retval
+ def _processAddons(self, addon_paths): + # collect all kickstart addons to 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.AddonData)) + if classes: + addon_class = classes[0] + + # create data structure + addons[addon_id] = addon_class(name = addon_id) + for commandName, commandClass in addon_class.COMMAND_MAP.iteritems(): + # check for conflicts + if commandName in self.commands: + raise KeyError("Trying to override kickstart command %s" % commandName) + + # register all commands + cmdObj = commandClass(commandName = commandName, data = addons[addon_id]) + self.commands[commandName] = cmdObj + + # Also, add the object into the _writeOrder dict in the right place. + if cmdObj.writePriority is not None: + if not self._writeOrder.has_key(cmdObj.writePriority): + self._writeOrder[cmdObj.writePriority] = [cmdObj] + else: + self._writeOrder[cmdObj.writePriority].append(cmdObj) + + + # Prepare the final structures for 3rd party addons + self.addons = AddonRegistry(addons) + + def _insertSorted(self, lst, obj): length = len(lst) i = 0 diff --git a/pykickstart/parser.py b/pykickstart/parser.py index c37ec62..de939b2 100644 --- a/pykickstart/parser.py +++ b/pykickstart/parser.py @@ -46,6 +46,7 @@ from errors import KickstartError, KickstartParseError, KickstartValueError, for from ko import KickstartObject from sections import * import version +from addons import AddonSection
import gettext _ = lambda x: gettext.ldgettext("pykickstart", x) @@ -728,3 +729,4 @@ class KickstartParser: self.registerSection(PostScriptSection(self.handler, dataObj=Script)) self.registerSection(TracebackScriptSection(self.handler, dataObj=Script)) self.registerSection(PackageSection(self.handler)) + self.registerSection(AddonSection(self.handler))
--- pykickstart/base.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-)
diff --git a/pykickstart/base.py b/pykickstart/base.py index f9cc161..14d407c 100644 --- a/pykickstart/base.py +++ b/pykickstart/base.py @@ -335,14 +335,7 @@ class BaseHandler(KickstartObject): # register all commands cmdObj = commandClass(commandName = commandName, data = addons[addon_id]) self.commands[commandName] = cmdObj - - # Also, add the object into the _writeOrder dict in the right place. - if cmdObj.writePriority is not None: - if not self._writeOrder.has_key(cmdObj.writePriority): - self._writeOrder[cmdObj.writePriority] = [cmdObj] - else: - self._writeOrder[cmdObj.writePriority].append(cmdObj) - + self._addToWriteOrder(cmdObj)
# Prepare the final structures for 3rd party addons self.addons = AddonRegistry(addons) @@ -379,7 +372,9 @@ class BaseHandler(KickstartObject): name = unicode(cmdObj.__class__.__name__).lower()
setattr(self, name.lower(), cmdObj) + self._addToWriteOrder(cmdObj)
+ def _addToWriteOrder(self, cmdObj): # Also, add the object into the _writeOrder dict in the right place. if cmdObj.writePriority is not None: if self._writeOrder.has_key(cmdObj.writePriority):
anaconda-patches@lists.fedorahosted.org