summaryrefslogtreecommitdiffstats
path: root/ipapython
diff options
context:
space:
mode:
authorJan Cholasta <jcholast@redhat.com>2015-06-02 12:04:25 +0000
committerJan Cholasta <jcholast@redhat.com>2015-06-08 15:34:11 +0000
commit9e9c01fba2938f26843a6c4f44622c86416ef525 (patch)
tree7c7253d28f91d90949cbf9ec6b0e19215bcf07e0 /ipapython
parent08229a0c5457d4e0c13d6b02a1f38f60ea787856 (diff)
downloadfreeipa-9e9c01fba2938f26843a6c4f44622c86416ef525.tar.gz
freeipa-9e9c01fba2938f26843a6c4f44622c86416ef525.tar.xz
freeipa-9e9c01fba2938f26843a6c4f44622c86416ef525.zip
install: Introduce installer framework ipapython.install
https://fedorahosted.org/freeipa/ticket/4468 Reviewed-By: Martin Basti <mbasti@redhat.com>
Diffstat (limited to 'ipapython')
-rw-r--r--ipapython/Makefile1
-rw-r--r--ipapython/install/__init__.py7
-rw-r--r--ipapython/install/cli.py255
-rw-r--r--ipapython/install/common.py115
-rw-r--r--ipapython/install/core.py532
-rw-r--r--ipapython/install/util.py169
-rw-r--r--ipapython/setup.py.in4
7 files changed, 1082 insertions, 1 deletions
diff --git a/ipapython/Makefile b/ipapython/Makefile
index b2cf719fd..852764323 100644
--- a/ipapython/Makefile
+++ b/ipapython/Makefile
@@ -10,6 +10,7 @@ all:
(cd $$subdir && $(MAKE) $@) || exit 1; \
done
+.PHONY: install
install:
if [ "$(DESTDIR)" = "" ]; then \
python2 setup.py install; \
diff --git a/ipapython/install/__init__.py b/ipapython/install/__init__.py
new file mode 100644
index 000000000..2be73027c
--- /dev/null
+++ b/ipapython/install/__init__.py
@@ -0,0 +1,7 @@
+#
+# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
+#
+
+"""
+Installer framework.
+"""
diff --git a/ipapython/install/cli.py b/ipapython/install/cli.py
new file mode 100644
index 000000000..b83fd9a2f
--- /dev/null
+++ b/ipapython/install/cli.py
@@ -0,0 +1,255 @@
+#
+# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
+#
+
+"""
+Command line support.
+"""
+
+import collections
+import optparse
+import signal
+
+from ipapython import admintool
+from ipapython.ipautil import CheckedIPAddress, private_ccache
+
+from . import core, common
+
+__all__ = ['install_tool', 'uninstall_tool']
+
+
+def install_tool(configurable_class, command_name, log_file_name,
+ debug_option=False, uninstall_log_file_name=None):
+ if uninstall_log_file_name is not None:
+ uninstall_kwargs = dict(
+ configurable_class=configurable_class,
+ command_name=command_name,
+ log_file_name=uninstall_log_file_name,
+ debug_option=debug_option,
+ )
+ else:
+ uninstall_kwargs = None
+
+ return type(
+ 'install_tool({0})'.format(configurable_class.__name__),
+ (InstallTool,),
+ dict(
+ configurable_class=configurable_class,
+ command_name=command_name,
+ log_file_name=log_file_name,
+ debug_option=debug_option,
+ uninstall_kwargs=uninstall_kwargs,
+ )
+ )
+
+
+def uninstall_tool(configurable_class, command_name, log_file_name,
+ debug_option=False):
+ return type(
+ 'uninstall_tool({0})'.format(configurable_class.__name__),
+ (UninstallTool,),
+ dict(
+ configurable_class=configurable_class,
+ command_name=command_name,
+ log_file_name=log_file_name,
+ debug_option=debug_option,
+ )
+ )
+
+
+class ConfigureTool(admintool.AdminTool):
+ configurable_class = None
+ debug_option = False
+
+ @staticmethod
+ def _transform(configurable_class):
+ raise NotImplementedError
+
+ @classmethod
+ def add_options(cls, parser):
+ basic_group = optparse.OptionGroup(parser, "basic options")
+
+ groups = collections.OrderedDict()
+ groups[None] = basic_group
+
+ transformed_cls = cls._transform(cls.configurable_class)
+ for owner_cls, name in transformed_cls.knobs():
+ knob_cls = getattr(owner_cls, name)
+ if not knob_cls.initializable:
+ continue
+
+ group_cls = owner_cls.group()
+ try:
+ opt_group = groups[group_cls]
+ except KeyError:
+ opt_group = groups[group_cls] = optparse.OptionGroup(
+ parser, "{0} options".format(group_cls.description))
+
+ kwargs = dict()
+ if knob_cls.type is bool:
+ kwargs['type'] = None
+ elif knob_cls.type is int:
+ kwargs['type'] = 'int'
+ elif knob_cls.type is long:
+ kwargs['type'] = 'long'
+ elif knob_cls.type is float:
+ kwargs['type'] = 'float'
+ elif knob_cls.type is complex:
+ kwargs['type'] = 'complex'
+ elif isinstance(knob_cls.type, set):
+ kwargs['type'] = 'choice'
+ kwargs['choices'] = list(knob_cls.type)
+ else:
+ kwargs['type'] = 'string'
+ kwargs['dest'] = name
+ kwargs['action'] = 'callback'
+ kwargs['callback'] = cls._option_callback
+ kwargs['callback_args'] = (knob_cls,)
+ if knob_cls.sensitive:
+ kwargs['sensitive'] = True
+ if knob_cls.cli_metavar:
+ kwargs['metavar'] = knob_cls.cli_metavar
+
+ if knob_cls.cli_short_name:
+ short_opt_str = '-{0}'.format(knob_cls.cli_short_name)
+ else:
+ short_opt_str = ''
+ cli_name = knob_cls.cli_name or name
+ opt_str = '--{0}'.format(cli_name.replace('_', '-'))
+ if not knob_cls.deprecated:
+ help = knob_cls.description
+ else:
+ help = optparse.SUPPRESS_HELP
+ opt_group.add_option(
+ short_opt_str, opt_str,
+ help=help,
+ **kwargs
+ )
+
+ if knob_cls.cli_aliases:
+ opt_group.add_option(
+ *knob_cls.cli_aliases,
+ help=optparse.SUPPRESS_HELP,
+ **kwargs
+ )
+
+ if issubclass(transformed_cls, common.Interactive):
+ basic_group.add_option(
+ '-U', '--unattended',
+ dest='unattended',
+ default=False,
+ action='store_true',
+ help="unattended (un)installation never prompts the user",
+ )
+
+ for group, opt_group in groups.iteritems():
+ parser.add_option_group(opt_group)
+
+ super(ConfigureTool, cls).add_options(parser,
+ debug_option=cls.debug_option)
+
+ @classmethod
+ def _option_callback(cls, option, opt_str, value, parser, knob):
+ if knob.type is bool:
+ value_type = bool
+ is_list = False
+ value = True
+ else:
+ if isinstance(knob.type, tuple):
+ assert knob.type[0] is list
+ value_type = knob.type[1]
+ is_list = True
+ else:
+ value_type = knob.type
+ is_list = False
+
+ if value_type == 'ip':
+ value_type = CheckedIPAddress
+ elif value_type == 'ip-local':
+ value_type = lambda v: CheckedIPAddress(v, match_local=True)
+
+ try:
+ value = value_type(value)
+ except ValueError as e:
+ raise optparse.OptionValueError(
+ "option {0}: {1}".format(opt_str, e))
+
+ if is_list:
+ old_value = getattr(parser.values, option.dest) or []
+ old_value.append(value)
+ value = old_value
+
+ setattr(parser.values, option.dest, value)
+
+ def validate_options(self, needs_root=True):
+ super(ConfigureTool, self).validate_options(needs_root=needs_root)
+
+ def run(self):
+ kwargs = {}
+
+ transformed_cls = self._transform(self.configurable_class)
+ for owner_cls, name in transformed_cls.knobs():
+ value = getattr(self.options, name, None)
+ if value is not None:
+ kwargs[name] = value
+
+ if (issubclass(self.configurable_class, common.Interactive) and
+ not self.options.unattended):
+ kwargs['interactive'] = True
+
+ try:
+ cfgr = transformed_cls(**kwargs)
+ except core.KnobValueError as e:
+ knob_cls = getattr(transformed_cls, e.name)
+ cli_name = knob_cls.cli_name or e.name
+ opt_str = '--{0}'.format(cli_name.replace('_', '-'))
+ self.option_parser.error("option {0}: {1}".format(opt_str, e))
+ except RuntimeError as e:
+ self.option_parser.error(str(e))
+
+ signal.signal(signal.SIGTERM, self.__signal_handler)
+
+ # Use private ccache
+ with private_ccache():
+ super(ConfigureTool, self).run()
+
+ cfgr.run()
+
+ @staticmethod
+ def __signal_handler(signum, frame):
+ raise KeyboardInterrupt
+
+
+class InstallTool(ConfigureTool):
+ uninstall_kwargs = None
+
+ _transform = staticmethod(common.installer)
+
+ @classmethod
+ def add_options(cls, parser):
+ super(InstallTool, cls).add_options(parser)
+
+ if cls.uninstall_kwargs is not None:
+ uninstall_group = optparse.OptionGroup(parser, "uninstall options")
+ uninstall_group.add_option(
+ '--uninstall',
+ dest='uninstall',
+ default=False,
+ action='store_true',
+ help=("uninstall an existing installation. The uninstall can "
+ "be run with --unattended option"),
+ )
+ parser.add_option_group(uninstall_group)
+
+ @classmethod
+ def get_command_class(cls, options, args):
+ if cls.uninstall_kwargs is not None and options.uninstall:
+ uninstall_cls = uninstall_tool(**cls.uninstall_kwargs)
+ uninstall_cls.option_parser = cls.option_parser
+ return uninstall_cls
+ else:
+ return super(InstallTool, cls).get_command_class(options, args)
+
+
+class UninstallTool(ConfigureTool):
+ _transform = staticmethod(common.uninstaller)
diff --git a/ipapython/install/common.py b/ipapython/install/common.py
new file mode 100644
index 000000000..799ce5009
--- /dev/null
+++ b/ipapython/install/common.py
@@ -0,0 +1,115 @@
+#
+# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
+#
+
+"""
+Common stuff.
+"""
+
+import traceback
+
+from . import core
+from .util import from_
+
+__all__ = ['step', 'Installable', 'Interactive', 'Continuous', 'installer',
+ 'uninstaller']
+
+
+def step():
+ def decorator(func):
+ cls = core.Component(Step)
+ cls._installer = staticmethod(func)
+ return cls
+
+ return decorator
+
+
+class Installable(core.Configurable):
+ """
+ Configurable which does install or uninstall.
+ """
+
+ uninstalling = core.Property(False)
+
+ def _get_components(self):
+ components = super(Installable, self)._get_components()
+ if self.uninstalling:
+ components = reversed(list(components))
+ return components
+
+ def _configure(self):
+ if self.uninstalling:
+ return self._uninstall()
+ else:
+ return self._install()
+
+ def _install(self):
+ assert not hasattr(super(Installable, self), '_install')
+
+ return super(Installable, self)._configure()
+
+ def _uninstall(self):
+ assert not hasattr(super(Installable, self), '_uninstall')
+
+ return super(Installable, self)._configure()
+
+
+class Step(Installable):
+ @property
+ def parent(self):
+ raise AttributeError('parent')
+
+ def _install(self):
+ for nothing in self._installer(self.parent):
+ yield from_(super(Step, self)._install())
+
+ @staticmethod
+ def _installer(obj):
+ yield
+
+ def _uninstall(self):
+ for nothing in self._uninstaller(self.parent):
+ yield from_(super(Step, self)._uninstall())
+
+ @staticmethod
+ def _uninstaller(obj):
+ yield
+
+ @classmethod
+ def uninstaller(cls, func):
+ cls._uninstaller = staticmethod(func)
+ return cls
+
+
+class Interactive(core.Configurable):
+ interactive = core.Property(False)
+
+
+class Continuous(core.Configurable):
+ def _handle_exception(self, exc_info):
+ try:
+ super(Continuous, self)._handle_exception(exc_info)
+ except BaseException as e:
+ self.log.debug(traceback.format_exc())
+ if isinstance(e, Exception):
+ self.log.error("%s", e)
+
+
+def installer(cls):
+ class Installer(cls, Installable):
+ def __init__(self, **kwargs):
+ super(Installer, self).__init__(uninstalling=False,
+ **kwargs)
+ Installer.__name__ = 'installer({0})'.format(cls.__name__)
+
+ return Installer
+
+
+def uninstaller(cls):
+ class Uninstaller(Continuous, cls, Installable):
+ def __init__(self, **kwargs):
+ super(Uninstaller, self).__init__(uninstalling=True,
+ **kwargs)
+ Uninstaller.__name__ = 'uninstaller({0})'.format(cls.__name__)
+
+ return Uninstaller
diff --git a/ipapython/install/core.py b/ipapython/install/core.py
new file mode 100644
index 000000000..c313c278e
--- /dev/null
+++ b/ipapython/install/core.py
@@ -0,0 +1,532 @@
+#
+# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
+#
+
+"""
+The framework core.
+"""
+
+import sys
+import abc
+import itertools
+
+from ipapython.ipa_log_manager import root_logger
+
+from . import util
+from .util import from_
+
+__all__ = ['InvalidStateError', 'KnobValueError', 'Property', 'Knob',
+ 'Configurable', 'Group', 'Component', 'Composite']
+
+# Configurable states
+_VALIDATE_PENDING = 'VALIDATE_PENDING'
+_VALIDATE_RUNNING = 'VALIDATE_RUNNING'
+_EXECUTE_PENDING = 'EXECUTE_PENDING'
+_EXECUTE_RUNNING = 'EXECUTE_RUNNING'
+_STOPPED = 'STOPPED'
+_FAILED = 'FAILED'
+_CLOSED = 'CLOSED'
+
+_missing = object()
+_counter = itertools.count()
+
+
+def _class_cmp(a, b):
+ if a is b:
+ return 0
+ elif issubclass(a, b):
+ return -1
+ elif issubclass(b, a):
+ return 1
+ else:
+ return 0
+
+
+class InvalidStateError(Exception):
+ pass
+
+
+class KnobValueError(ValueError):
+ def __init__(self, name, message):
+ super(KnobValueError, self).__init__(message)
+ self.name = name
+
+
+class InnerClass(object):
+ __metaclass__ = util.InnerClassMeta
+ __outer_class__ = None
+ __outer_name__ = None
+
+
+class PropertyBase(InnerClass):
+ @property
+ def default(self):
+ raise AttributeError('default')
+
+ def __init__(self, outer):
+ self.outer = outer
+
+ def __get__(self, obj, obj_type):
+ try:
+ return obj._get_property(self.__outer_name__)
+ except AttributeError:
+ if not hasattr(self, 'default'):
+ raise
+ return self.default
+
+
+def Property(default=_missing):
+ class_dict = {}
+ if default is not _missing:
+ class_dict['default'] = default
+
+ return util.InnerClassMeta('Property', (PropertyBase,), class_dict)
+
+
+class KnobBase(PropertyBase):
+ type = None
+ initializable = True
+ sensitive = False
+ deprecated = False
+ description = None
+ cli_name = None
+ cli_short_name = None
+ cli_aliases = None
+ cli_metavar = None
+
+ _order = None
+
+ def __set__(self, obj, value):
+ try:
+ self.validate(value)
+ except KnobValueError:
+ raise
+ except ValueError as e:
+ raise KnobValueError(self.__outer_name__, str(e))
+
+ obj.__dict__[self.__outer_name__] = value
+
+ def __delete__(self, obj):
+ try:
+ del obj.__dict__[self.__outer_name__]
+ except KeyError:
+ raise AttributeError(self.__outer_name__)
+
+ def validate(self, value):
+ pass
+
+ @classmethod
+ def default_getter(cls, func):
+ @property
+ def default(self):
+ return func(self.outer)
+ cls.default = default
+
+ return cls
+
+ @classmethod
+ def validator(cls, func):
+ def validate(self, value):
+ func(self.outer, value)
+ super(cls, self).validate(value)
+ cls.validate = validate
+
+ return cls
+
+
+def Knob(type, default=_missing, initializable=_missing, sensitive=_missing,
+ deprecated=_missing, description=_missing, cli_name=_missing,
+ cli_short_name=_missing, cli_aliases=_missing, cli_metavar=_missing):
+ class_dict = {}
+ class_dict['_order'] = next(_counter)
+ class_dict['type'] = type
+ if default is not _missing:
+ class_dict['default'] = default
+ if sensitive is not _missing:
+ class_dict['sensitive'] = sensitive
+ if deprecated is not _missing:
+ class_dict['deprecated'] = deprecated
+ if description is not _missing:
+ class_dict['description'] = description
+ if cli_name is not _missing:
+ class_dict['cli_name'] = cli_name
+ if cli_short_name is not _missing:
+ class_dict['cli_short_name'] = cli_short_name
+ if cli_aliases is not _missing:
+ class_dict['cli_aliases'] = cli_aliases
+ if cli_metavar is not _missing:
+ class_dict['cli_metavar'] = cli_metavar
+
+ return util.InnerClassMeta('Knob', (KnobBase,), class_dict)
+
+
+class Configurable(object):
+ """
+ Base class of all configurables.
+
+ FIXME: details of validate/execute, properties and knobs
+ """
+
+ __metaclass__ = abc.ABCMeta
+
+ @classmethod
+ def knobs(cls):
+ """
+ Iterate over knobs defined for the configurable.
+ """
+
+ assert not hasattr(super(Configurable, cls), 'knobs')
+
+ result = []
+ for name in dir(cls):
+ knob_cls = getattr(cls, name)
+ if isinstance(knob_cls, type) and issubclass(knob_cls, KnobBase):
+ result.append(knob_cls)
+ result = sorted(result, key=lambda knob_cls: knob_cls._order)
+ for knob_cls in result:
+ yield knob_cls.__outer_class__, knob_cls.__outer_name__
+
+ @classmethod
+ def group(cls):
+ assert not hasattr(super(Configurable, cls), 'group')
+
+ return None
+
+ def __init__(self, **kwargs):
+ """
+ Initialize the configurable.
+ """
+
+ self.log = root_logger
+
+ for name in dir(self.__class__):
+ if name.startswith('_'):
+ continue
+ property_cls = getattr(self.__class__, name)
+ if not isinstance(property_cls, type):
+ continue
+ if not issubclass(property_cls, PropertyBase):
+ continue
+ if issubclass(property_cls, KnobBase):
+ continue
+ try:
+ value = kwargs.pop(name)
+ except KeyError:
+ pass
+ else:
+ setattr(self, name, value)
+
+ for owner_cls, name in self.knobs():
+ knob_cls = getattr(owner_cls, name)
+ if not knob_cls.initializable:
+ continue
+
+ try:
+ value = kwargs.pop(name)
+ except KeyError:
+ pass
+ else:
+ setattr(self, name, value)
+
+ if kwargs:
+ extra = sorted(kwargs.keys())
+ raise TypeError(
+ "{0}() got {1} unexpected keyword arguments: {2}".format(
+ type(self).__name__,
+ len(extra),
+ ', '.join(repr(name) for name in extra)))
+
+ self._reset()
+
+ def _reset(self):
+ assert not hasattr(super(Configurable, self), '_reset')
+
+ self.__state = _VALIDATE_PENDING
+ self.__gen = util.run_generator_with_yield_from(self._configure())
+
+ def _get_components(self):
+ assert not hasattr(super(Configurable, self), '_get_components')
+
+ raise TypeError("{0} is not composite".format(self))
+
+ def _get_property(self, name):
+ assert not hasattr(super(Configurable, self), '_get_property')
+
+ try:
+ return self.__dict__[name]
+ except KeyError:
+ raise AttributeError(name)
+
+ @abc.abstractmethod
+ def _configure(self):
+ """
+ Coroutine which defines the logic of the configurable.
+ """
+
+ assert not hasattr(super(Configurable, self), '_configure')
+
+ self.__transition(_VALIDATE_RUNNING, _EXECUTE_PENDING)
+
+ while self.__state != _EXECUTE_RUNNING:
+ yield
+
+ def run(self):
+ """
+ Run the configurable.
+ """
+
+ self.validate()
+ if self.__state == _EXECUTE_PENDING:
+ self.execute()
+
+ def validate(self):
+ """
+ Run the validation part of the configurable.
+ """
+
+ for nothing in self._validator():
+ pass
+
+ def _validator(self):
+ """
+ Coroutine which runs the validation part of the configurable.
+ """
+
+ return self.__runner(_VALIDATE_PENDING, _VALIDATE_RUNNING)
+
+ def execute(self):
+ """
+ Run the execution part of the configurable.
+ """
+
+ for nothing in self._executor():
+ pass
+
+ def _executor(self):
+ """
+ Coroutine which runs the execution part of the configurable.
+ """
+
+ return self.__runner(_EXECUTE_PENDING, _EXECUTE_RUNNING)
+
+ def done(self):
+ """
+ Return True if the configurable has finished.
+ """
+
+ return self.__state in (_STOPPED, _FAILED, _CLOSED)
+
+ def run_until_executing(self, gen):
+ while self.__state != _EXECUTE_RUNNING:
+ try:
+ yield gen.next()
+ except StopIteration:
+ break
+
+ def __runner(self, pending_state, running_state):
+ self.__transition(pending_state, running_state)
+
+ step = self.__gen.next
+ while True:
+ try:
+ step()
+ except StopIteration:
+ self.__transition(running_state, _STOPPED)
+ break
+ except GeneratorExit:
+ self.__transition(running_state, _CLOSED)
+ break
+ except BaseException:
+ exc_info = sys.exc_info()
+ try:
+ self._handle_exception(exc_info)
+ except BaseException:
+ raise
+ else:
+ break
+ finally:
+ self.__transition(running_state, _FAILED)
+
+ if self.__state != running_state:
+ break
+
+ try:
+ yield
+ except BaseException:
+ exc_info = sys.exc_info()
+ step = lambda: self.__gen.throw(*exc_info)
+ else:
+ step = self.__gen.next
+
+ def _handle_exception(self, exc_info):
+ assert not hasattr(super(Configurable, self), '_handle_exception')
+
+ util.raise_exc_info(exc_info)
+
+ def __transition(self, from_state, to_state):
+ if self.__state != from_state:
+ raise InvalidStateError(self.__state)
+
+ self.__state = to_state
+
+
+class Group(Configurable):
+ @classmethod
+ def group(cls):
+ return cls
+
+
+class ComponentMeta(util.InnerClassMeta, abc.ABCMeta):
+ pass
+
+
+class ComponentBase(InnerClass, Configurable):
+ __metaclass__ = ComponentMeta
+
+ _order = None
+
+ @classmethod
+ def group(cls):
+ result = super(ComponentBase, cls).group()
+ if result is not None:
+ return result
+ else:
+ return cls.__outer_class__.group()
+
+ def __init__(self, parent, **kwargs):
+ self.__parent = parent
+
+ super(ComponentBase, self).__init__(**kwargs)
+
+ @property
+ def parent(self):
+ return self.__parent
+
+ def __get__(self, obj, obj_type):
+ obj.__dict__[self.__outer_name__] = self
+ return self
+
+ def _get_property(self, name):
+ try:
+ return super(ComponentBase, self)._get_property(name)
+ except AttributeError:
+ return self.__parent._get_property(name)
+
+ def _handle_exception(self, exc_info):
+ try:
+ super(ComponentBase, self)._handle_exception(exc_info)
+ except BaseException:
+ exc_info = sys.exc_info()
+ self.__parent._handle_exception(exc_info)
+
+
+def Component(cls):
+ class_dict = {}
+ class_dict['_order'] = next(_counter)
+
+ return ComponentMeta('Component', (ComponentBase, cls), class_dict)
+
+
+class Composite(Configurable):
+ """
+ Configurable composed of any number of components.
+
+ Provides knobs of all child components.
+ """
+
+ @classmethod
+ def knobs(cls):
+ name_dict = {}
+ owner_dict = {}
+
+ for owner_cls, name in super(Composite, cls).knobs():
+ knob_cls = getattr(owner_cls, name)
+ name_dict[name] = owner_cls
+ owner_dict.setdefault(owner_cls, []).append(knob_cls)
+
+ for owner_cls, name in cls.components():
+ comp_cls = getattr(cls, name)
+ for owner_cls, name in comp_cls.knobs():
+ if hasattr(cls, name):
+ continue
+
+ knob_cls = getattr(owner_cls, name)
+ try:
+ last_owner_cls = name_dict[name]
+ except KeyError:
+ name_dict[name] = owner_cls
+ owner_dict.setdefault(owner_cls, []).append(knob_cls)
+ else:
+ if last_owner_cls is not owner_cls:
+ raise TypeError("{0}.knobs(): conflicting definitions "
+ "of '{1}' in {2} and {3}".format(
+ cls.__name__,
+ name,
+ last_owner_cls.__name__,
+ owner_cls.__name__))
+
+ for owner_cls in sorted(owner_dict, _class_cmp):
+ for knob_cls in owner_dict[owner_cls]:
+ yield knob_cls.__outer_class__, knob_cls.__outer_name__
+
+ @classmethod
+ def components(cls):
+ assert not hasattr(super(Composite, cls), 'components')
+
+ result = []
+ for name in dir(cls):
+ comp_cls = getattr(cls, name)
+ if (isinstance(comp_cls, type) and
+ issubclass(comp_cls, ComponentBase)):
+ result.append(comp_cls)
+ result = sorted(result, key=lambda comp_cls: comp_cls._order)
+ for comp_cls in result:
+ yield comp_cls.__outer_class__, comp_cls.__outer_name__
+
+ def _reset(self):
+ self.__components = list(self._get_components())
+
+ super(Composite, self)._reset()
+
+ def _get_components(self):
+ for owner_cls, name in self.components():
+ yield getattr(self, name)
+
+ def _configure(self):
+ validate = [(c, c._validator()) for c in self.__components]
+ while True:
+ new_validate = []
+ for child, validator in validate:
+ try:
+ validator.next()
+ except StopIteration:
+ if child.done():
+ self.__components.remove(child)
+ else:
+ new_validate.append((child, validator))
+ if not new_validate:
+ break
+ validate = new_validate
+
+ yield
+
+ if not self.__components:
+ return
+
+ yield from_(super(Composite, self)._configure())
+
+ execute = [(c, c._executor()) for c in self.__components]
+ while True:
+ new_execute = []
+ for child, executor in execute:
+ try:
+ executor.next()
+ except StopIteration:
+ pass
+ else:
+ new_execute.append((child, executor))
+ if not new_execute:
+ break
+ execute = new_execute
+
+ yield
diff --git a/ipapython/install/util.py b/ipapython/install/util.py
new file mode 100644
index 000000000..58da7bb77
--- /dev/null
+++ b/ipapython/install/util.py
@@ -0,0 +1,169 @@
+#
+# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
+#
+
+"""
+Utilities.
+"""
+
+import sys
+
+
+def raise_exc_info(exc_info):
+ """
+ Raise exception from exception info tuple as returned by `sys.exc_info()`.
+ """
+
+ raise exc_info[0], exc_info[1], exc_info[2]
+
+
+class from_(object):
+ """
+ Wrapper for delegating to a subgenerator.
+
+ See `run_generator_with_yield_from`.
+ """
+ __slots__ = ('obj',)
+
+ def __init__(self, obj):
+ self.obj = obj
+
+
+def run_generator_with_yield_from(gen):
+ """
+ Iterate over a generator object with subgenerator delegation.
+
+ This implements Python 3's ``yield from`` expressions, using Python 2
+ syntax:
+
+ >>> def subgen():
+ ... yield 'B'
+ ... yield 'C'
+ ...
+ >>> def gen():
+ ... yield 'A'
+ ... yield from_(subgen())
+ ... yield 'D'
+ ...
+ >>> list(run_generator_with_yield_from(gen()))
+ ['A', 'B', 'C', 'D']
+
+ Returning value from a subgenerator is not supported.
+ """
+
+ exc_info = None
+ value = None
+
+ stack = [gen]
+ while stack:
+ prev_exc_info, exc_info = exc_info, None
+ prev_value, value = value, None
+
+ gen = stack[-1]
+ try:
+ if prev_exc_info is None:
+ value = gen.send(prev_value)
+ else:
+ value = gen.throw(*prev_exc_info)
+ except StopIteration:
+ stack.pop()
+ continue
+ except BaseException:
+ exc_info = sys.exc_info()
+ stack.pop()
+ continue
+ else:
+ if isinstance(value, from_):
+ stack.append(value.obj)
+ value = None
+ continue
+
+ try:
+ value = (yield value)
+ except BaseException:
+ exc_info = sys.exc_info()
+
+ if exc_info is not None:
+ raise_exc_info(exc_info)
+
+
+class InnerClassMeta(type):
+ def __new__(cls, name, bases, class_dict):
+ class_dict.pop('__outer_class__', None)
+ class_dict.pop('__outer_name__', None)
+
+ return super(InnerClassMeta, cls).__new__(cls, name, bases, class_dict)
+
+ def __get__(self, obj, obj_type):
+ outer_class, outer_name = self.__bind(obj_type)
+ if obj is None:
+ return self
+ assert isinstance(obj, outer_class)
+
+ try:
+ return obj.__dict__[outer_name]
+ except KeyError:
+ inner = self(obj)
+ try:
+ getter = inner.__get__
+ except AttributeError:
+ return inner
+ else:
+ return getter(obj, obj_type)
+
+ def __set__(self, obj, value):
+ outer_class, outer_name = self.__bind(obj.__class__)
+ assert isinstance(obj, outer_class)
+
+ inner = self(obj)
+ try:
+ setter = inner.__set__
+ except AttributeError:
+ try:
+ inner.__delete__
+ except AttributeError:
+ obj.__dict__[outer_name] = value
+ else:
+ raise AttributeError('__set__')
+ else:
+ setter(obj, value)
+
+ def __delete__(self, obj):
+ outer_class, outer_name = self.__bind(obj.__class__)
+ assert isinstance(obj, outer_class)
+
+ inner = self(obj)
+ try:
+ deleter = inner.__delete__
+ except AttributeError:
+ try:
+ inner.__set__
+ except AttributeError:
+ try:
+ del obj.__dict__[outer_name]
+ except KeyError:
+ raise AttributeError(outer_name)
+ else:
+ raise AttributeError('__delete__')
+ else:
+ deleter(obj)
+
+ def __bind(self, obj_type):
+ try:
+ cls = self.__dict__['__outer_class__']
+ name = self.__dict__['__outer_name__']
+ except KeyError:
+ cls, name, value = None, None, None
+ for cls in obj_type.__mro__:
+ for name, value in cls.__dict__.iteritems():
+ if value is self:
+ break
+ if value is self:
+ break
+ assert value is self
+
+ self.__outer_class__ = cls
+ self.__outer_name__ = name
+ self.__name__ = '.'.join((cls.__name__, name))
+
+ return cls, name
diff --git a/ipapython/setup.py.in b/ipapython/setup.py.in
index 6caf17905..6cba59cfc 100644
--- a/ipapython/setup.py.in
+++ b/ipapython/setup.py.in
@@ -65,7 +65,9 @@ def setup_package():
classifiers=filter(None, CLASSIFIERS.split('\n')),
platforms = ["Linux", "Solaris", "Unix"],
package_dir = {'ipapython': ''},
- packages = [ "ipapython", "ipapython.dnssec" ],
+ packages = ["ipapython",
+ "ipapython.dnssec",
+ "ipapython.install"],
)
finally:
del sys.path[0]