diff --git a/TODO b/TODO index 5199dfa..e69de29 100644 --- a/TODO +++ b/TODO @@ -1 +0,0 @@ -- add real plugin factory \ No newline at end of file diff --git a/molecule/settings.py b/molecule/settings.py index a3c1d3f..de20b36 100644 --- a/molecule/settings.py +++ b/molecule/settings.py @@ -18,8 +18,6 @@ import os from molecule.exception import SpecFileError -from molecule.specs.plugins import SPEC_PLUGS -from molecule.specs.plugins.builtin import LivecdSpec from molecule.specs.skel import GenericSpec from molecule.version import VERSION import molecule.utils @@ -82,18 +80,24 @@ class Configuration(dict): class SpecParser: - # FIXME: kept for backward .spec files compatibility where - # execution_strategy argument is not set - DEFAULT_PARSER = LivecdSpec - def __init__(self, filepath): self.filepath = filepath[:] + + # FIXME: kept for backward .spec files compatibility where + # execution_strategy argument is not set + # TODO: remove and make execution_strategy mandatory in .spec file + from molecule.specs.plugins.builtin_plugin import LivecdSpec + execution_strategy = self.parse_execution_strategy() if execution_strategy is None: - execution_strategy = SpecParser.DEFAULT_PARSER.execution_strategy() + execution_strategy = LivecdSpec.execution_strategy() - plugin = SPEC_PLUGS.get(execution_strategy) + from molecule.specs.factory import PluginFactory + spec_plugins = PluginFactory.get_spec_plugins() + print spec_plugins + + plugin = spec_plugins.get(execution_strategy) if plugin is None: raise SpecFileError("Execution strategy provided in %s spec file" " not supported, strategy: %s" % ( @@ -132,6 +136,7 @@ class SpecParser: # compact lines properly old_key = None for line in data: + key = None if ":" in line: key, value = self.parse_line_statement(line) if key is None: diff --git a/molecule/specs/factory.py b/molecule/specs/factory.py new file mode 100644 index 0000000..12b47fb --- /dev/null +++ b/molecule/specs/factory.py @@ -0,0 +1,306 @@ +# -*- coding: utf-8 -*- +# Molecule Disc Image builder for Sabayon Linux +# Copyright (C) 2009 Fabio Erculiani +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty 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., 675 Mass Ave, Cambridge, MA 02139, USA. +import sys +import os +import inspect + +class PluginFactory: + + """ + Generic Molecule Components Plugin Factory (loader). + """ + + _PLUGIN_SUFFIX = "_plugin" + _PYTHON_EXTENSION = ".py" + + def __init__(self, base_plugin_class, plugin_package_module, + default_plugin_name = None, fallback_plugin_name = None, + egg_entry_point_group = None): + """ + Molecule Generic Plugin Factory constructor. + MANDATORY: every plugin module/package(name) must end with _plugin + suffix. + + Base plugin classes must have the following class attributes set: + + - BASE_PLUGIN_API_VERSION: integer describing API revision in use + in class + + Subclasses of Base plugin class must have the following class + attributes set: + + - PLUGIN_API_VERSION: integer describing the currently implemented + plugin API revision, must match with BASE_PLUGIN_API_VERSION + above otherwise plugin won't be loaded and a warning will be + printed. + + Moreover, plugin classes must be "Python new-style classes", otherwise + parser won't be able to determine if classes have subclasses and thus + pick the proper object (one with no subclasses!!). + See: http://www.python.org/doc/newstyle -- in other words, you have + to inherit the built-in "object" class (yeah, it's called object). + So, even if using normal classes could work, if you start doing nasty + things (nested inherittance of plugin classes), behaviour cannot + be guaranteed. + If it's not clear, let me repeat once again, valid plugin classes + must not have subclasses around! Think about it, it's an obvious thing. + + If plugin class features a "PLUGIN_DISABLED" class attribute with + a boolean value of True, such plugin will be ignored. + + If egg_entry_point_group is specified, Python Egg support is enabled + and classes are loaded via this infrastructure. + NOTE: if egg_entry_point_group is set, you NEED the setuptools package. + + @param base_plugin_class: Base class that valid plugin classes must + inherit from. + @type base_plugin_class: class + @param plugin_package_module: every plugin repository must work as + Python package, the value of this argument must be a valid + Python package module that can be scanned looking for valid + Molecule Plugin classes. + @type plugin_package_module: Python module + @keyword default_plugin_name: identifier of the default plugin to load + @type default_plugin_name: string + @keyword fallback_plugin_name: identifier of the fallback plugin to load + if default is not available + @type fallback_plugin_name: string + @keyword egg_entry_point_group: valid Python Egg entry point group, in + this case, Python Egg support is used + @type egg_entry_point_group: string + @raise AttributeError: when passed plugin_package_module is not a + valid Python package module + """ + self.__modfile = plugin_package_module.__file__ + self.__base_class = base_plugin_class + self.__plugin_package_module = plugin_package_module + self.__default_plugin_name = default_plugin_name + self.__fallback_plugin_name = fallback_plugin_name + self.__egg_entry_group = egg_entry_point_group + self.__cache = None + self.__inspect_cache = None + + def clear_cache(self): + """ + Clear available plugins cache. When calling get_available_plugins() + module object is parsed again. + """ + self.__cache = None + self.__inspect_cache = None + + def _inspect_object(self, obj): + """ + This method verifies if given object is a valid plugin. + + @return: True, if valid + @rtype: bool + """ + + if self.__inspect_cache is None: + self.__inspect_cache = {} + + # use memory position + obj_memory_pos = id(obj) + # To avoid infinite recursion and improve + # objects inspection, check if obj has been already + # analyzed + inspected_rc = self.__inspect_cache.get(obj_memory_pos) + if inspected_rc is not None: + # avoid infinite recursion + return inspected_rc + + base_api = self.__base_class.BASE_PLUGIN_API_VERSION + + if not inspect.isclass(obj): + self.__inspect_cache[obj_memory_pos] = False + return False + + if not issubclass(obj, self.__base_class): + self.__inspect_cache[obj_memory_pos] = False + return False + + if hasattr(obj, '__subclasses__'): + # new style class + if obj.__subclasses__(): # only lower classes taken + self.__inspect_cache[obj_memory_pos] = False + return False + else: + sys.stderr.write("!!! Molecule Plugin warning: " \ + "%s is not a new style class !!!\n" % (obj,)) + + if obj is self.__base_class: + # in this case, obj is our base class, + # so we are very sure that obj is not valid + self.__inspect_cache[obj_memory_pos] = False + return False + + if not hasattr(obj, "PLUGIN_API_VERSION"): + sys.stderr.write("!!! Molecule Plugin warning: " \ + "no PLUGIN_API_VERSION in %s !!!\n" % (obj,)) + self.__inspect_cache[obj_memory_pos] = False + return False + + if obj.PLUGIN_API_VERSION != base_api: + sys.stderr.write("!!! Molecule Plugin warning: " \ + "PLUGIN_API_VERSION mismatch in %s !!!\n" % (obj,)) + self.__inspect_cache[obj_memory_pos] = False + return False + + if hasattr(obj, 'PLUGIN_DISABLED'): + if obj.PLUGIN_DISABLED: + # this plugin has been disabled + self.__inspect_cache[obj_memory_pos] = False + return False + + self.__inspect_cache[obj_memory_pos] = True + return True + + def _scan_dir(self): + """ + Scan modules in given directory looking for a valid plugin class. + Directory is os.path.dirname(self.__modfile). + + @return: module dictionary composed by module name as key and plugin + class as value + @rtype: dict + """ + available = {} + pkg_modname = self.__plugin_package_module.__name__ + mod_dir = os.path.dirname(self.__modfile) + + for modname in os.listdir(mod_dir): + + if modname.startswith("__"): + continue # python stuff + if not (modname.endswith(PluginFactory._PYTHON_EXTENSION) \ + or "." not in modname): + continue # not something we want to load + + if modname.endswith(PluginFactory._PYTHON_EXTENSION): + modname = modname[:-len(PluginFactory._PYTHON_EXTENSION)] + + if not modname.endswith(PluginFactory._PLUGIN_SUFFIX): + continue + + # remove suffix + modname_clean = modname[:-len(PluginFactory._PLUGIN_SUFFIX)] + + modpath = "%s.%s" % (pkg_modname, modname,) + + try: + __import__(modpath) + except ImportError as err: + sys.stderr.write("!!! Molecule Plugin warning, cannot " \ + "load module: %s | %s !!!\n" % (modpath, err,)) + continue + + for obj in list(sys.modules[modpath].__dict__.values()): + + valid = self._inspect_object(obj) + if not valid: + continue + + available[modname_clean] = obj + + return available + + def _scan_egg_group(self): + """ + Scan classes in given Python Egg group name looking for a valid plugin. + + @return: module dictionary composed by module name as key and plugin + class as value + @rtype: dict + """ + # needs setuptools + import pkg_resources + available = {} + + for entry in pkg_resources.iter_entry_points(self.__egg_entry_group): + + obj = entry.load() + valid = self._inspect_object(obj) + if not valid: + continue + available[entry.name] = obj + + return available + + + def get_available_plugins(self): + """ + Return currently available plugin classes. + Note: Molecule plugins can either be Python packages or modules and + their name MUST end with PluginFactory._PLUGIN_SUFFIX ("_plugin"). + + @return: dictionary composed by Molecule plugin id as key and Molecule + Python module as value + @rtype: dict + """ + if self.__cache is not None: + return self.__cache.copy() + + if self.__egg_entry_group: + available = self._scan_egg_group() + else: + available = self._scan_dir() + + self.__cache = available.copy() + return available + + def get_default_plugin(self): + """ + Return currently configured Molecule Plugin class. + + @return: Molecule plugin class + @rtype: default plugin class object + @raise KeyError: if default plugin is not set or not found + """ + available = self.get_available_plugins() + plugin = self.__default_plugin_name + fallback = self.__fallback_plugin_name + klass = available.get(plugin) + + if klass is None: + import warnings + warnings.warn("%s: %s" % ( + "selected Plugin not available, using fallback", plugin,)) + klass = available.get(fallback) + + if klass is None: + raise KeyError + + return klass + + _SPEC_FACTORY = None + + @staticmethod + def _get_spec(): + if PluginFactory._SPEC_FACTORY is None: + from molecule.specs.skel import GenericSpec + from . import plugins as plugs + PluginFactory._SPEC_FACTORY = PluginFactory(GenericSpec, plugs) + return PluginFactory._SPEC_FACTORY + + @staticmethod + def get_spec_plugins(): + factory = PluginFactory._get_spec() + plugins = factory.get_available_plugins() + spec_plugins = dict((y.execution_strategy(), y) for x, y \ + in plugins.items()) + return spec_plugins diff --git a/molecule/specs/plugins/__init__.py b/molecule/specs/plugins/__init__.py index 71add4a..a2adb65 100644 --- a/molecule/specs/plugins/__init__.py +++ b/molecule/specs/plugins/__init__.py @@ -15,11 +15,3 @@ # 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., 675 Mass Ave, Cambridge, MA 02139, USA. - -from molecule.specs.plugins.builtin import LivecdSpec -from molecule.specs.plugins.remaster import RemasterSpec -from molecule.specs.plugins.tar import IsoToTarSpec - -# FIXME: this will need to be pluggable (and plugin factory is required) -SPEC_PLUGS = dict((x.execution_strategy(), x,) for x in \ - (LivecdSpec, RemasterSpec, IsoToTarSpec)) diff --git a/molecule/specs/plugins/builtin.py b/molecule/specs/plugins/builtin_plugin.py similarity index 99% rename from molecule/specs/plugins/builtin.py rename to molecule/specs/plugins/builtin_plugin.py index df1a5f5..6c99b04 100644 --- a/molecule/specs/plugins/builtin.py +++ b/molecule/specs/plugins/builtin_plugin.py @@ -589,6 +589,8 @@ class IsoHandler(GenericExecutionStep, BuiltinHandlerMixin): class LivecdSpec(GenericSpec): + PLUGIN_API_VERSION = 0 + @staticmethod def execution_strategy(): return "livecd" diff --git a/molecule/specs/plugins/remaster.py b/molecule/specs/plugins/remaster_plugin.py similarity index 97% rename from molecule/specs/plugins/remaster.py rename to molecule/specs/plugins/remaster_plugin.py index 1562451..0da761e 100644 --- a/molecule/specs/plugins/remaster.py +++ b/molecule/specs/plugins/remaster_plugin.py @@ -24,10 +24,13 @@ from molecule.i18n import _ from molecule.output import red, brown, blue, green, purple, darkgreen, \ darkred, bold, darkblue, readtext from molecule.specs.skel import GenericExecutionStep, GenericSpec -from molecule.specs.plugins.builtin import ChrootHandler as BuiltinChrootHandler -from molecule.specs.plugins.builtin import CdrootHandler as BuiltinCdrootHandler -from molecule.specs.plugins.builtin import IsoHandler as BuiltinIsoHandler -from molecule.specs.plugins.builtin import BuiltinHandlerMixin +from molecule.specs.plugins.builtin_plugin import ChrootHandler as \ + BuiltinChrootHandler +from molecule.specs.plugins.builtin_plugin import CdrootHandler as \ + BuiltinCdrootHandler +from molecule.specs.plugins.builtin_plugin import IsoHandler as \ + BuiltinIsoHandler +from molecule.specs.plugins.builtin_plugin import BuiltinHandlerMixin import molecule.utils class IsoUnpackHandler(GenericExecutionStep, BuiltinHandlerMixin): @@ -308,6 +311,8 @@ class IsoHandler(BuiltinIsoHandler): class RemasterSpec(GenericSpec): + PLUGIN_API_VERSION = 0 + @staticmethod def execution_strategy(): return "iso_remaster" diff --git a/molecule/specs/plugins/tar.py b/molecule/specs/plugins/tar_plugin.py similarity index 97% rename from molecule/specs/plugins/tar.py rename to molecule/specs/plugins/tar_plugin.py index 349a02e..1f290a9 100644 --- a/molecule/specs/plugins/tar.py +++ b/molecule/specs/plugins/tar_plugin.py @@ -23,8 +23,8 @@ import shutil from molecule.i18n import _ from molecule.output import blue, darkred from molecule.specs.skel import GenericExecutionStep, GenericSpec -from molecule.specs.plugins.builtin import BuiltinHandlerMixin -from molecule.specs.plugins.remaster import IsoUnpackHandler, \ +from molecule.specs.plugins.builtin_plugin import BuiltinHandlerMixin +from molecule.specs.plugins.remaster_plugin import IsoUnpackHandler, \ ChrootHandler import molecule.utils @@ -108,6 +108,8 @@ class TarHandler(GenericExecutionStep, BuiltinHandlerMixin): class IsoToTarSpec(GenericSpec): + PLUGIN_API_VERSION = 0 + @staticmethod def execution_strategy(): return "iso_to_tar" diff --git a/molecule/specs/skel.py b/molecule/specs/skel.py index 51a1424..da8d757 100644 --- a/molecule/specs/skel.py +++ b/molecule/specs/skel.py @@ -142,10 +142,13 @@ class GenericExecutionStep: raise NotImplementedError() -class GenericSpec(GenericSpecFunctions): +class GenericSpec(object, GenericSpecFunctions): EXECUTION_STRATEGY_KEY = "execution_strategy" + # Molecule Plugin factory support + BASE_PLUGIN_API_VERSION = 0 + @staticmethod def execution_strategy(): """