Dne 16.4.2015 v 16:46 Jan Cholasta napsal(a):
Hi,

the attached patch adds the basics of the new installer framework.

As a next step, I plan to convert the install scripts to use the
framework with their old code (the old code will be gradually ported to
the framework later).

(Note I didn't manage to write docstrings today, expect update tomorrow.)

Added some docstrings.

Also updated the patch to reflect little brainstorming David and I had this morning.


Honza

--
Jan Cholasta
>From 2245f992185a6f03b27afa50b10c811797c6dda4 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 16 Apr 2015 14:35:24 +0000
Subject: [PATCH] install: Introduce installer framework ipapython.install

---
 ipapython/install.py | 468 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 468 insertions(+)
 create mode 100644 ipapython/install.py

diff --git a/ipapython/install.py b/ipapython/install.py
new file mode 100644
index 0000000..1c0c646
--- /dev/null
+++ b/ipapython/install.py
@@ -0,0 +1,468 @@
+# Authors:
+#   Jan Cholasta <jchol...@redhat.com>
+#
+# Copyright (C) 2015  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+#
+
+"""
+Installer framework.
+"""
+
+import sys
+import inspect
+import abc
+import itertools
+
+_VALIDATE_PENDING = 'VALIDATE_PENDING'
+_VALIDATE_RUNNING = 'VALIDATE_RUNNING'
+_EXECUTE_PENDING = 'EXECUTE_PENDING'
+_EXECUTE_RUNNING = 'EXECUTE_RUNNING'
+_STOPPED = 'STOPPED'
+_FAILED = 'FAILED'
+_CLOSED = 'CLOSED'
+
+_missing = object()
+
+
+class from_(object):
+    __slots__ = ('obj',)
+
+    def __init__(self, obj):
+        self.obj = obj
+
+
+def _run_generator_with_yield_from(gen):
+    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_):
+                value = None
+                stack.append(value.obj)
+                continue
+
+        try:
+            value = (yield value)
+        except BaseException:
+            exc_info = sys.exc_info()
+
+    if exc_info:
+        raise exc_info  #pylint: disable=raising-bad-type
+
+
+class InvalidStateError(Exception):
+    pass
+
+
+class Knob(object):
+    """
+    Public argument of a Configurator.
+    """
+
+    __counter = itertools.count()
+
+    def __init__(self, type, default=_missing, label=None):
+        """
+        Initialize the knob.
+        """
+        self.type = type
+        if default is _missing:
+            self.required = True
+            self.default = None
+        else:
+            self.required = False
+            self.default = default
+        self.label = label
+        self.order = next(self.__counter)
+
+    def _set(self, obj, value):
+        setattr(obj, '_knob{0}'.format(id(self)), value)
+
+    def __get__(self, obj, obj_type):
+        if obj is None:
+            return self
+        return getattr(obj, '_knob{0}'.format(id(self)))
+
+
+class Configurator(object):
+    """
+    Base class of all configurators.
+
+    FIXME: details of validate/execute, args and knobs
+    """
+
+    __metaclass__ = abc.ABCMeta
+
+    @classmethod
+    def args(cls):
+        """
+        Iterate over arguments of the configurator.
+        """
+        for super_cls in cls.__mro__:
+            if not issubclass(super_cls, Configurator):
+                continue
+
+            try:
+                init = super_cls.__dict__['__init__']
+            except KeyError:
+                continue
+
+            argspec = inspect.getargspec(init)
+            for name in argspec.args[1:]:
+                if not name.startswith('_'):
+                    yield name
+
+        for knob_cls, name, obj in cls.knobs():
+            yield name
+
+    @classmethod
+    def knobs(cls):
+        """
+        Iterate over knobs defined for the configurator.
+        """
+        result = []
+        for name in dir(cls):
+            obj = getattr(cls, name)
+            if isinstance(obj, Knob):
+                result.append((cls, name, obj))
+        return iter(sorted(result, key=lambda knob: knob[2].order))
+
+    def __init__(self, **kwargs):
+        """
+        Initialize the configurator.
+        """
+        for cls, name, knob in self.knobs():
+            try:
+                value = kwargs.pop(name)
+            except KeyError:
+                if knob.required:
+                    continue
+                value = knob.default
+            setattr(self, name, value)
+
+        missing = set()
+        for name in self.args():
+            try:
+                getattr(self, name)
+            except AttributeError:
+                missing.add(name)
+
+        if missing:
+            missing = sorted(missing)
+            raise TypeError(
+                "{0}() did not set {1} required attributes: {2}".format(
+                    type(self).__name__,
+                    len(missing),
+                    ', '.join(repr(name) for name in missing)))
+
+        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.__state = _VALIDATE_PENDING
+        self.__gen = _run_generator_with_yield_from(self._generator())
+
+    @abc.abstractmethod
+    def _generator(self):
+        """
+        Coroutine which defines the logic of the configurator.
+        """
+        self.__transition(_VALIDATE_RUNNING, _EXECUTE_PENDING)
+
+        while self.__state != _EXECUTE_RUNNING:
+            yield
+
+    def validate(self):
+        """
+        Run the validation part of the configurator.
+        """
+        for nothing in self.validator():
+            pass
+
+    def validator(self):
+        """
+        Coroutine which runs the validation part of the configurator.
+        """
+        return self.__runner(_VALIDATE_PENDING, _VALIDATE_RUNNING)
+
+    def execute(self):
+        """
+        Run the execution part of the configurator.
+        """
+        for nothing in self.executor():
+            pass
+
+    def executor(self):
+        """
+        Coroutine which runs the execution part of the configurator.
+        """
+        return self.__runner(_EXECUTE_PENDING, _EXECUTE_RUNNING)
+
+    def done(self):
+        """
+        Return True if the configurator 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)
+
+        gen_next = self.__gen.next
+        while True:
+            try:
+                gen_next()
+            except StopIteration:
+                self.__transition(running_state, _STOPPED)
+                break
+            except GeneratorExit:
+                self.__transition(running_state, _CLOSED)
+                break
+            except BaseException:
+                self.__transition(running_state, _FAILED)
+                raise
+
+            if self.__state != running_state:
+                break
+
+            try:
+                yield
+            except BaseException:
+                exc_info = sys.exc_info()
+                gen_next = lambda: self.__gen.throw(*exc_info)
+            else:
+                gen_next = self.__gen.next
+
+    def __transition(self, from_state, to_state):
+        if self.__state != from_state:
+            raise InvalidStateError(self.__state)
+
+        self.__state = to_state
+
+
+class ComposableConfigurator(Configurator):
+    """
+    Configurator composable into a composite configurator.
+
+    Arguments which are not specified in the constructor are inherited from
+    the parent composite configurator.
+    """
+
+    def __init__(self, parent, **kwargs):
+        self.parent = parent
+
+        super(ComposableConfigurator, self).__init__(**kwargs)
+
+    def __setattr__(self, name, value):
+        if name in self.args() and value is _missing:
+            return
+
+        super(ComposableConfigurator, self).__setattr__(name, value)
+
+    def __getattr__(self, name):
+        if name in self.args():
+            return getattr(self.parent, name)
+
+        raise AttributeError(name)
+
+
+class CompositeConfigurator(Configurator):
+    """
+    Configurator composed of any number of composable configurators.
+
+    Provides knobs of all child composable configurators.
+    """
+
+    @classmethod
+    def knobs(cls):
+        for child_cls in cls.children():
+            for knob in child_cls.knobs():
+                yield knob
+
+        for knob in super(CompositeConfigurator, cls).knobs():
+            yield knob
+
+    @classmethod
+    def children(cls):
+        raise NotImplementedError
+
+    def __init__(self, **kwargs):
+        self.__children = None
+
+        super(CompositeConfigurator, self).__init__(**kwargs)
+
+        self.__children = tuple(self._init_children())
+
+    def _init_children(self):
+        for cls in self.children():
+            kwargs = dict.fromkeys(cls.args(), _missing)
+            kwargs['parent'] = self
+            yield cls(**kwargs)
+
+    def _generator(self):
+        to_validate = self.__children
+        to_execute = list(self.__children)
+
+        validate = [(c, c.validator()) for c in to_validate]
+        while True:
+            new_validate = []
+            for child, validator in validate:
+                try:
+                    validator.next()
+                except StopIteration:
+                    if child.done():
+                        to_execute = [c for c in to_execute if c is not child]
+                else:
+                    new_validate.append((child, validator))
+            if not new_validate:
+                break
+            validate = new_validate
+
+            yield
+
+        if not to_execute:
+            return
+
+        yield from_(super(CompositeConfigurator, self)._generator())
+
+        execute = [(c, c.executor()) for c in to_execute]
+        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
+
+
+class Installer(Configurator):
+    """
+    Configurator which does install or uninstall.
+    """
+
+    def __init__(self, uninstall=False, **kwargs):
+        self.uninstall = uninstall
+
+        super(Installer, self).__init__(**kwargs)
+
+    def _generator(self):
+        if self.uninstall:
+            return self._uninstall_generator()
+        else:
+            return self._install_generator()
+
+    def _install_generator(self):
+        return super(Installer, self)._generator()
+
+    def _uninstall_generator(self):
+        return super(Installer, self)._generator()
+
+
+class ComposableInstaller(Installer, ComposableConfigurator):
+    pass
+
+
+class CompositeInstaller(Installer, CompositeConfigurator):
+    def _init_children(self):
+        children = super(CompositeInstaller, self)._init_children()
+        if self.uninstall:
+            children = reversed(tuple(children))
+        return children
+
+
+class InstallerWithSteps(CompositeInstaller):
+    @classmethod
+    def children(cls):
+        steps = []
+        for name in dir(cls):
+            obj = getattr(cls, name)
+            if hasattr(obj, '_step_order'):
+                steps.append(obj)
+
+        steps = sorted(steps, key=lambda step: step._step_order)
+        for step in steps:
+            yield step
+
+
+class InstallerStep(ComposableInstaller):
+    __counter = itertools.count()
+
+    @classmethod
+    def subclass(cls, func):
+        cls = type(func.__name__, (cls,),
+                   dict(__doc__=func.__doc__,
+                        _step_order=next(cls.__counter)))
+
+        def _install_generator(self):
+            super_generator = super(cls, self)._install_generator()
+            return func(self.parent, super_generator)
+        cls._install_generator = _install_generator
+
+        return cls
+
+    @classmethod
+    def uninstaller(cls, func):
+        assert cls is not InstallerStep
+
+        def _uninstall_generator(self):
+            super_generator = super(cls, self)._uninstall_generator()
+            return func(self.parent, super_generator)
+        cls._uninstall_generator = _uninstall_generator
+
+        return cls
+
+
+def step():
+    def decorator(func):
+        return InstallerStep.subclass(func)
+
+    return decorator
-- 
2.1.0

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to