---
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(a)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))
--
1.7.11.7