Dne 11.5.2015 v 13:41 Jan Cholasta napsal(a):
Dne 6.5.2015 v 08:22 Jan Cholasta napsal(a):
Dne 6.5.2015 v 08:11 Martin Kosek napsal(a):
On 04/29/2015 06:25 PM, Jan Cholasta wrote:
Dne 20.4.2015 v 16:56 Jan Cholasta napsal(a):
Dne 20.4.2015 v 15:14 Martin Basti napsal(a):
On 17/04/15 16:15, Jan Cholasta wrote:
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



Hello, see comments bellow:

1) We started using new shorter License header in files:
#
# Copyright (C) 2015  FreeIPA Contributors see COPYING for license
#

OK.


2) IMO this will not work, NoneType has no 'obj' attribute
+        else:
+            if isinstance(value, from_):
+                value = None
+                stack.append(value.obj)
+                continue

Right.


3) Multiple inheritance. I do not like it much.
+class CompositeInstaller(Installer, CompositeConfigurator):

I guess you are antagonistic to multiple inheritance because of how
other languages (like C++) do it. In Python it can be pretty elegant
and
is basis for e.g. the mixin design pattern.


Installer and CompositeConfigurator inherites from Configurator
class,
and all of them implements _generator method.

Both of them call super()._generator(), so it's no problem (same for
other methods).


If I understand correctly
(https://www.python.org/download/releases/2.3/mro/) the
Installer._generator method will be used in this case.
However in case when CompositeConfigurator has more levels
(respectively
it is more specialized) of inheritance, it could take precedence
and its
_generator method may be used instead.

The order of precedence is defined by the order of base classes in the
class definition.


I'm afraid this may suddenly stop working.
Maybe I'm wrong, please fix me.

As long as you call the super class, it will work fine.


And Multiple inheritance is not easily readable, this is even a
diamond
inheritance model.

Cooperative inheritance is used by design and IMHO is easily
readable if
you know how to read it. Every class defines a single bit of behavior.
Without cooperative inheritance, it would have to be hardcoded and/or
hacked around, which I wanted to avoid.

This blog post explains it nicely:
<https://rhettinger.wordpress.com/2011/05/26/super-considered-super/>.


Updated patch attached.

Also attached is patch 425 which migrates ipa-server-install to the
install
framework.

Good job there. I am just curious, will this framework and new option
processing be friendly to other types of option passing than just via
options?
I mean tickets

https://fedorahosted.org/freeipa/ticket/4517
https://fedorahosted.org/freeipa/ticket/4468

Especially 4517 is important, we need to be able to run

# cat install.conf
ds_password=Secret123
admin_password=Secret456
ip_address=123456
setup_dns=False

# ipa-server-install --unattended --conf install.conf

I assume yes, but I am just making sure.

Yes, definitely.


Updated patches attached.

Another update, patches attached.

--
Jan Cholasta
>From 1426620a2bc89ab74689ca6ede075ebc398bab9e Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 2 Jun 2015 12:04:25 +0000
Subject: [PATCH 1/2] install: Introduce installer framework ipapython.install

https://fedorahosted.org/freeipa/ticket/4468
---
 freeipa.spec.in               |   2 +
 ipapython/Makefile            |   1 +
 ipapython/install/__init__.py |   7 +
 ipapython/install/cli.py      | 253 +++++++++++++++++++++
 ipapython/install/common.py   | 114 ++++++++++
 ipapython/install/core.py     | 506 ++++++++++++++++++++++++++++++++++++++++++
 ipapython/install/util.py     | 169 ++++++++++++++
 ipapython/setup.py.in         |   4 +-
 8 files changed, 1055 insertions(+), 1 deletion(-)
 create mode 100644 ipapython/install/__init__.py
 create mode 100644 ipapython/install/cli.py
 create mode 100644 ipapython/install/common.py
 create mode 100644 ipapython/install/core.py
 create mode 100644 ipapython/install/util.py

diff --git a/freeipa.spec.in b/freeipa.spec.in
index 09dd66e..bedcb6d 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -882,6 +882,8 @@ fi
 %{python_sitelib}/ipapython/*.py*
 %dir %{python_sitelib}/ipapython/dnssec
 %{python_sitelib}/ipapython/dnssec/*.py*
+%dir %{python_sitelib}/ipapython/install
+%{python_sitelib}/ipapython/install/*.py*
 %dir %{python_sitelib}/ipalib
 %{python_sitelib}/ipalib/*
 %dir %{python_sitelib}/ipaplatform
diff --git a/ipapython/Makefile b/ipapython/Makefile
index b2cf719..8527643 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 0000000..2be7302
--- /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 0000000..edf2a0c
--- /dev/null
+++ b/ipapython/install/cli.py
@@ -0,0 +1,253 @@
+#
+# 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
+
+from . import 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):
+        signal.signal(signal.SIGTERM, self.__signal_handler)
+
+        kwargs = {}
+        if (issubclass(self.configurable_class, common.Interactive) and
+                not self.options.unattended):
+            kwargs['interactive'] = True
+
+        transformed_cls = self._transform(self.configurable_class)
+        cfgr = transformed_cls(**kwargs)
+
+        for owner_cls, name in cfgr.knobs():
+            knob_cls = getattr(owner_cls, name)
+
+            value = getattr(self.options, name, None)
+            if value is None:
+                continue
+
+            try:
+                setattr(cfgr, name, value)
+            except ValueError as e:
+                cli_name = knob_cls.cli_name or name
+                opt_str = '--{0}'.format(cli_name.replace('_', '-'))
+                self.option_parser.error("option {0}: {1}".format(opt_str, e))
+
+        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 0000000..3de8137
--- /dev/null
+++ b/ipapython/install/common.py
@@ -0,0 +1,114 @@
+#
+# 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())
+            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 0000000..d284eb4
--- /dev/null
+++ b/ipapython/install/core.py
@@ -0,0 +1,506 @@
+#
+# 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', '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 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):
+        self.validate(value)
+        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
+            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 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_name__
+
+    def _reset(self):
+        self.__components = list(self._get_components())
+
+        super(Composite, self)._reset()
+
+    def _get_components(self):
+        for 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 0000000..58da7bb
--- /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 6caf179..6cba59c 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]
-- 
2.1.0

>From 7f130cba5b165389c8f8c8915001f2cad91aeaf9 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 2 Jun 2015 12:36:14 +0000
Subject: [PATCH 2/2] install: Migrate ipa-server-install to the install
 framework

https://fedorahosted.org/freeipa/ticket/4468
---
 install/tools/ipa-server-install     | 310 ++-----------------------
 ipaserver/install/server/__init__.py |   5 +-
 ipaserver/install/server/install.py  | 437 ++++++++++++++++++++++++++++++++++-
 3 files changed, 452 insertions(+), 300 deletions(-)

diff --git a/install/tools/ipa-server-install b/install/tools/ipa-server-install
index c7fa5ae..de465a4 100755
--- a/install/tools/ipa-server-install
+++ b/install/tools/ipa-server-install
@@ -20,247 +20,23 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
-
-# requires the following packages:
-# fedora-ds-base
-# openldap-clients
-# nss-tools
-
-import sys
-import os
-import signal
-import random
-from optparse import OptionGroup, OptionValueError, SUPPRESS_HELP
-
-from ipaserver.install import bindinstance
-from ipaserver.install import installutils
-from ipaserver.install import server
-from ipaserver.install.server import (
-    validate_dm_password, validate_admin_password)
-from ipapython import version
-
-from ipalib import constants
-from ipapython.config import IPAOptionParser
-from ipalib.util import validate_domain_name
-from ipapython.ipa_log_manager import root_logger, standard_logging_setup
-from ipapython.dn import DN
-
+from ipapython.install import cli
 from ipaplatform.paths import paths
-
-VALID_SUBJECT_ATTRS = ['st', 'o', 'ou', 'dnqualifier', 'c',
-                       'serialnumber', 'l', 'title', 'sn', 'givenname',
-                       'initials', 'generationqualifier', 'dc', 'mail',
-                       'uid', 'postaladdress', 'postalcode', 'postofficebox',
-                       'houseidentifier', 'e', 'street', 'pseudonym',
-                       'incorporationlocality', 'incorporationstate',
-                       'incorporationcountry', 'businesscategory']
-
-
-def subject_callback(option, opt_str, value, parser):
-    """
-    Make sure the certificate subject base is a valid DN
-    """
-    v = unicode(value, 'utf-8')
-    if any(ord(c) < 0x20 for c in v):
-        raise OptionValueError("Subject base must not contain control characters")
-    if '&' in v:
-        raise OptionValueError("Subject base must not contain an ampersand (\"&\")")
-    try:
-        dn = DN(v)
-        for rdn in dn:
-            if rdn.attr.lower() not in VALID_SUBJECT_ATTRS:
-                raise OptionValueError('%s=%s has invalid attribute: "%s"' % (opt_str, value, rdn.attr))
-    except ValueError, e:
-        raise OptionValueError('%s=%s has invalid subject base format: %s' % (opt_str, value, e))
-    parser.values.subject = dn
+from ipaserver.install.server import Server
 
 
-def parse_options():
-    # Guaranteed to give a random 200k range below the 2G mark (uint32_t limit)
-    namespace = random.randint(1, 10000) * 200000
-    parser = IPAOptionParser(version=version.VERSION)
+ServerInstall = cli.install_tool(
+    Server,
+    command_name='ipa-server-install',
+    log_file_name=paths.IPASERVER_INSTALL_LOG,
+    debug_option=True,
+    uninstall_log_file_name=paths.IPASERVER_UNINSTALL_LOG,
+)
 
-    basic_group = OptionGroup(parser, "basic options")
-    basic_group.add_option("-r", "--realm", dest="realm_name",
-                      help="realm name")
-    basic_group.add_option("-n", "--domain", dest="domain_name",
-                      help="domain name")
-    basic_group.add_option("-p", "--ds-password", dest="dm_password",
-                      sensitive=True, help="Directory Manager password")
-    basic_group.add_option("-P", "--master-password",
-                      dest="master_password", sensitive=True,
-                      help=SUPPRESS_HELP)
-    basic_group.add_option("-a", "--admin-password",
-                      sensitive=True, dest="admin_password",
-                      help="admin user kerberos password")
-    basic_group.add_option("--mkhomedir",
-                           dest="mkhomedir",
-                           action="store_true",
-                           default=False,
-                           help="create home directories for users "
-                                "on their first login")
-    basic_group.add_option("--hostname", dest="host_name", help="fully qualified name of server")
-    basic_group.add_option("--domain-level", dest="domainlevel", help="IPA domain level",
-                           default=constants.MAX_DOMAIN_LEVEL, type=int)
-    basic_group.add_option("--ip-address", dest="ip_addresses",
-                      type="ip", ip_local=True, action="append", default=[],
-                      help="Master Server IP Address. This option can be used multiple times",
-                      metavar="IP_ADDRESS")
-    basic_group.add_option("-N", "--no-ntp", dest="conf_ntp", action="store_false",
-                      help="do not configure ntp", default=True)
-    basic_group.add_option("--idstart", dest="idstart", default=namespace, type=int,
-                      help="The starting value for the IDs range (default random)")
-    basic_group.add_option("--idmax", dest="idmax", default=0, type=int,
-                      help="The max value value for the IDs range (default: idstart+199999)")
-    basic_group.add_option("--no_hbac_allow", dest="hbac_allow", default=False,
-                      action="store_true",
-                      help="Don't install allow_all HBAC rule")
-    basic_group.add_option("--no-ui-redirect", dest="ui_redirect", action="store_false",
-                      default=True, help="Do not automatically redirect to the Web UI")
-    basic_group.add_option("--ssh-trust-dns", dest="trust_sshfp", default=False, action="store_true",
-                      help="configure OpenSSH client to trust DNS SSHFP records")
-    basic_group.add_option("--no-ssh", dest="conf_ssh", default=True, action="store_false",
-                      help="do not configure OpenSSH client")
-    basic_group.add_option("--no-sshd", dest="conf_sshd", default=True, action="store_false",
-                      help="do not configure OpenSSH server")
-    basic_group.add_option("-d", "--debug", dest="debug", action="store_true",
-                      default=False, help="print debugging information")
-    basic_group.add_option("-U", "--unattended", dest="unattended", action="store_true",
-                      default=False, help="unattended (un)installation never prompts the user")
-    parser.add_option_group(basic_group)
 
-    cert_group = OptionGroup(parser, "certificate system options")
-    cert_group.add_option("", "--external-ca", dest="external_ca", action="store_true",
-                      default=False, help="Generate a CSR for the IPA CA certificate to be signed by an external CA")
-    cert_group.add_option("--external-ca-type", dest="external_ca_type",
-                      type="choice", choices=("generic", "ms-cs"),
-                      help="Type of the external CA")
-    cert_group.add_option("--external-cert-file", dest="external_cert_files",
-                      action="append", metavar="FILE",
-                      help="File containing the IPA CA certificate and the external CA certificate chain")
-    cert_group.add_option("--external_cert_file", dest="external_cert_files",
-                      action="append",
-                      help=SUPPRESS_HELP)
-    cert_group.add_option("--external_ca_file", dest="external_cert_files",
-                      action="append",
-                      help=SUPPRESS_HELP)
-    cert_group.add_option("--no-pkinit", dest="setup_pkinit", action="store_false",
-                      default=True, help="disables pkinit setup steps")
-    cert_group.add_option("--dirsrv-cert-file", dest="dirsrv_cert_files",
-                      action="append", metavar="FILE",
-                      help="File containing the Directory Server SSL certificate and private key")
-    cert_group.add_option("--dirsrv_pkcs12", dest="dirsrv_cert_files",
-                      action="append",
-                      help=SUPPRESS_HELP)
-    cert_group.add_option("--http-cert-file", dest="http_cert_files",
-                      action="append", metavar="FILE",
-                      help="File containing the Apache Server SSL certificate and private key")
-    cert_group.add_option("--http_pkcs12", dest="http_cert_files",
-                      action="append",
-                      help=SUPPRESS_HELP)
-    cert_group.add_option("--pkinit-cert-file", dest="pkinit_cert_files",
-                      action="append", metavar="FILE",
-                      help="File containing the Kerberos KDC SSL certificate and private key")
-    cert_group.add_option("--pkinit_pkcs12", dest="pkinit_cert_files",
-                      action="append",
-                      help=SUPPRESS_HELP)
-    cert_group.add_option("--dirsrv-pin", dest="dirsrv_pin", sensitive=True,
-                      metavar="PIN",
-                      help="The password to unlock the Directory Server private key")
-    cert_group.add_option("--dirsrv_pin", dest="dirsrv_pin", sensitive=True,
-                      help=SUPPRESS_HELP)
-    cert_group.add_option("--http-pin", dest="http_pin", sensitive=True,
-                      metavar="PIN",
-                      help="The password to unlock the Apache Server private key")
-    cert_group.add_option("--http_pin", dest="http_pin", sensitive=True,
-                      help=SUPPRESS_HELP)
-    cert_group.add_option("--pkinit-pin", dest="pkinit_pin", sensitive=True,
-                      metavar="PIN",
-                      help="The password to unlock the Kerberos KDC private key")
-    cert_group.add_option("--pkinit_pin", dest="pkinit_pin", sensitive=True,
-                      help=SUPPRESS_HELP)
-    cert_group.add_option("--dirsrv-cert-name", dest="dirsrv_cert_name",
-                      metavar="NAME",
-                      help="Name of the Directory Server SSL certificate to install")
-    cert_group.add_option("--http-cert-name", dest="http_cert_name",
-                      metavar="NAME",
-                      help="Name of the Apache Server SSL certificate to install")
-    cert_group.add_option("--pkinit-cert-name", dest="pkinit_cert_name",
-                      metavar="NAME",
-                      help="Name of the Kerberos KDC SSL certificate to install")
-    cert_group.add_option("--ca-cert-file", dest="ca_cert_files",
-                      action="append", metavar="FILE",
-                      help="File containing CA certificates for the service certificate files")
-    cert_group.add_option("--root-ca-file", dest="ca_cert_files",
-                      action="append",
-                      help=SUPPRESS_HELP)
-    cert_group.add_option("--subject", action="callback", callback=subject_callback,
-                      type="string",
-                      help="The certificate subject base (default O=<realm-name>)")
-    cert_group.add_option("--ca-signing-algorithm", dest="ca_signing_algorithm",
-                      type="choice",
-                      choices=('SHA1withRSA', 'SHA256withRSA', 'SHA512withRSA'),
-                      help="Signing algorithm of the IPA CA certificate")
-    parser.add_option_group(cert_group)
-
-    dns_group = OptionGroup(parser, "DNS options")
-    dns_group.add_option("--setup-dns", dest="setup_dns", action="store_true",
-                      default=False, help="configure bind with our zone")
-    dns_group.add_option("--forwarder", dest="forwarders", action="append",
-                      type="ip", help="Add a DNS forwarder. This option can be used multiple times")
-    dns_group.add_option("--no-forwarders", dest="no_forwarders", action="store_true",
-                      default=False, help="Do not add any DNS forwarders, use root servers instead")
-    dns_group.add_option("--reverse-zone", dest="reverse_zones",
-                      help="The reverse DNS zone to use. This option can be used multiple times",
-                      action="append", default=[], metavar="REVERSE_ZONE")
-    dns_group.add_option("--no-reverse", dest="no_reverse", action="store_true",
-                      default=False, help="Do not create reverse DNS zone")
-    dns_group.add_option("--no-dnssec-validation", dest="no_dnssec_validation", action="store_true",
-                      default=False, help="Disable DNSSEC validation")
-    dns_group.add_option("--zonemgr", action="callback", callback=bindinstance.zonemgr_callback,
-                      type="string",
-                      help="DNS zone manager e-mail address. Defaults to hostmaster@DOMAIN")
-    dns_group.add_option("--no-host-dns", dest="no_host_dns", action="store_true",
-                      default=False,
-                      help="Do not use DNS for hostname lookup during installation")
-    dns_group.add_option("--no-dns-sshfp", dest="create_sshfp", default=True, action="store_false",
-                      help="Do not automatically create DNS SSHFP records")
-    parser.add_option_group(dns_group)
-
-    uninstall_group = OptionGroup(parser, "uninstall options")
-    uninstall_group.add_option("", "--uninstall", dest="uninstall", action="store_true",
-                      default=False, help="uninstall an existing installation. The uninstall can " \
-                                          "be run with --unattended option")
-    parser.add_option_group(uninstall_group)
-
-    options, args = parser.parse_args()
-    safe_options = parser.get_safe_opts(options)
-
-    if options.dm_password is not None:
-        try:
-            validate_dm_password(options.dm_password)
-        except ValueError, e:
-            parser.error("DS admin password: " + str(e))
-    if options.admin_password is not None:
-        try:
-            validate_admin_password(options.admin_password)
-        except ValueError, e:
-            parser.error("Admin user password: " + str(e))
-
-    if options.domain_name is not None:
-        try:
-            validate_domain_name(options.domain_name)
-        except ValueError, e:
-            parser.error("invalid domain: " + unicode(e))
-
-    # Check that Domain Level is within the allowed range
-    if not options.uninstall:
-        if options.domainlevel < constants.MIN_DOMAIN_LEVEL:
-            parser.error("Domain Level cannot be lower than {0}"
-                         .format(constants.MIN_DOMAIN_LEVEL))
-        elif options.domainlevel > constants.MAX_DOMAIN_LEVEL:
-            parser.error("Domain Level cannot be higher than {0}"
-                         .format(constants.MAX_DOMAIN_LEVEL))
+def validate_options(self):
+    parser = self.option_parser
+    options = self.options
 
     if not options.setup_dns:
         if options.forwarders:
@@ -279,11 +55,11 @@ def parse_options():
         parser.error("You cannot specify a --reverse-zone option together with --no-reverse")
 
     if options.uninstall:
-        if (options.realm_name or
+        if (options.realm or
             options.admin_password or options.master_password):
             parser.error("In uninstall mode, -a, -r and -P options are not allowed")
     elif options.unattended:
-        if (not options.realm_name or
+        if (not options.realm or
             not options.dm_password or not options.admin_password):
             parser.error("In unattended mode you need to provide at least -r, -p and -a options")
         if options.setup_dns:
@@ -324,65 +100,11 @@ def parse_options():
         parser.error(
             "You cannot specify --external-ca-type without --external-ca")
 
-    if (options.external_cert_files and
-        any(not os.path.isabs(path) for path in options.external_cert_files)):
-        parser.error("--external-cert-file must use an absolute path")
-
-    if options.idmax == 0:
-        options.idmax = int(options.idstart) + 200000 - 1
-
     if options.idmax < options.idstart:
         parser.error("idmax (%u) cannot be smaller than idstart (%u)" %
                     (options.idmax, options.idstart))
 
-    #Automatically disable pkinit w/ dogtag until that is supported
-    options.setup_pkinit = False
-
-    options.dnssec_master = False
-
-    return safe_options, options
-
-
-def signal_handler(signum, frame):
-    raise KeyboardInterrupt
-
-
-def main():
-    safe_options, options = parse_options()
+ServerInstall.validate_options = validate_options
 
-    if os.getegid() != 0:
-        sys.exit("Must be root to set up server")
-
-    signal.signal(signal.SIGTERM, signal_handler)
-    signal.signal(signal.SIGINT, signal_handler)
-
-    if not options.uninstall:
-        standard_logging_setup(paths.IPASERVER_INSTALL_LOG,
-                               debug=options.debug)
-    else:
-        standard_logging_setup(paths.IPASERVER_UNINSTALL_LOG,
-                               debug=options.debug)
-
-    root_logger.debug('%s was invoked with options: %s' % (sys.argv[0], safe_options))
-    root_logger.debug("missing options might be asked for interactively later\n")
-    root_logger.debug('IPA version %s' % version.VENDOR_VERSION)
-
-    if not options.uninstall:
-        server.install_check(options)
-        server.install(options)
-    else:
-        server.uninstall_check(options)
-        server.uninstall(options)
-
-
-if __name__ == '__main__':
-    # FIXME: Common option parsing, logging setup, etc should be factored
-    # out from all install scripts
-    safe_options, options = parse_options()
-    if options.uninstall:
-        log_file_name = paths.IPASERVER_UNINSTALL_LOG
-    else:
-        log_file_name = paths.IPASERVER_INSTALL_LOG
 
-    installutils.run_script(main, log_file_name=log_file_name,
-                            operation_name='ipa-server-install')
+ServerInstall.run_cli()
diff --git a/ipaserver/install/server/__init__.py b/ipaserver/install/server/__init__.py
index 11879a8..a6db965 100644
--- a/ipaserver/install/server/__init__.py
+++ b/ipaserver/install/server/__init__.py
@@ -2,9 +2,8 @@
 # Copyright (C) 2015  FreeIPA Contributors see COPYING for license
 #
 
-from .install import install_check, install, uninstall_check, uninstall
+from .install import Server
+
 from .replicainstall import install_check as replica_install_check
 from .replicainstall import install as replica_install
 from .upgrade import upgrade_check, upgrade
-
-from .install import validate_dm_password, validate_admin_password
diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py
index aea1f99..216b373 100644
--- a/ipaserver/install/server/install.py
+++ b/ipaserver/install/server/install.py
@@ -5,6 +5,7 @@
 import os
 import pickle
 import pwd
+import random
 import shutil
 import sys
 import tempfile
@@ -12,13 +13,16 @@ import textwrap
 
 from ipapython import certmonger, dogtag, ipaldap, ipautil, sysrestore
 from ipapython.dn import DN
+from ipapython.install import common, core
+from ipapython.install.common import step
+from ipapython.install.core import Knob
 from ipapython.ipa_log_manager import root_logger
 from ipapython.ipautil import (
     decrypt_file, format_netloc, ipa_generate_password, run, user_input)
 from ipaplatform import services
 from ipaplatform.paths import paths
 from ipaplatform.tasks import tasks
-from ipalib import api, errors, x509
+from ipalib import api, constants, errors, x509
 from ipalib.constants import CACERT
 from ipalib.util import validate_domain_name
 import ipaclient.ntpconf
@@ -39,6 +43,14 @@ except ImportError:
 
 SYSRESTORE_DIR_PATH = paths.SYSRESTORE
 
+VALID_SUBJECT_ATTRS = ['st', 'o', 'ou', 'dnqualifier', 'c',
+                       'serialnumber', 'l', 'title', 'sn', 'givenname',
+                       'initials', 'generationqualifier', 'dc', 'mail',
+                       'uid', 'postaladdress', 'postalcode', 'postofficebox',
+                       'houseidentifier', 'e', 'street', 'pseudonym',
+                       'incorporationlocality', 'incorporationstate',
+                       'incorporationcountry', 'businesscategory']
+
 installation_cleanup = True
 original_ccache = None
 temp_ccache = None
@@ -693,7 +705,6 @@ def install(options):
     cfg = dict(
         context='installer',
         in_server=True,
-        debug=options.debug
     )
 
     # Figure out what external CA step we're in. See cainstance.py for more
@@ -1030,7 +1041,6 @@ def uninstall_check(options):
     cfg = dict(
         context='installer',
         in_server=True,
-        debug=options.debug
     )
 
     # We will need at least api.env, finalize api now. This system is
@@ -1212,3 +1222,424 @@ def uninstall(options):
     destroy_private_ccache()
 
     sys.exit(rv)
+
+
+class ServerCA(common.Installable, core.Group, core.Composite):
+    description = "certificate system"
+
+    external_ca = Knob(
+        bool, False,
+        description=("Generate a CSR for the IPA CA certificate to be signed "
+                     "by an external CA"),
+    )
+
+    external_ca_type = Knob(
+        {'generic', 'ms-cs'}, None,
+        description="Type of the external CA",
+    )
+
+    external_cert_files = Knob(
+        (list, str), None,
+        description=("File containing the IPA CA certificate and the external "
+                     "CA certificate chain (can be specified multiple times)"),
+        cli_name='external_cert_file',
+        cli_aliases=['--external_cert_file', '--external_ca_file'],
+        cli_metavar='FILE',
+    )
+
+    @external_cert_files.validator
+    def external_cert_files(self, value):
+        if any(not os.path.isabs(path) for path in value):
+            raise ValueError("must use an absolute path")
+
+    no_pkinit = Knob(
+        bool, False,
+        description="disables pkinit setup steps",
+    )
+
+    dirsrv_cert_files = Knob(
+        (list, str), None,
+        description=("File containing the Directory Server SSL certificate "
+                     "and private key"),
+        cli_name='dirsrv_cert_file',
+        cli_aliases=['--dirsrv_pkcs12'],
+        cli_metavar='FILE',
+    )
+
+    http_cert_files = Knob(
+        (list, str), None,
+        description=("File containing the Apache Server SSL certificate and "
+                     "private key"),
+        cli_name='http_cert_file',
+        cli_aliases=['--http_pkcs12'],
+        cli_metavar='FILE',
+    )
+
+    pkinit_cert_files = Knob(
+        (list, str), None,
+        description=("File containing the Kerberos KDC SSL certificate and "
+                     "private key"),
+        cli_name='pkinit_cert_file',
+        cli_aliases=['--pkinit_pkcs12'],
+        cli_metavar='FILE',
+    )
+
+    dirsrv_pin = Knob(
+        str, None,
+        sensitive=True,
+        description="The password to unlock the Directory Server private key",
+        cli_aliases=['--dirsrv_pin'],
+        cli_metavar='PIN',
+    )
+
+    http_pin = Knob(
+        str, None,
+        sensitive=True,
+        description="The password to unlock the Apache Server private key",
+        cli_aliases=['--http_pin'],
+        cli_metavar='PIN',
+    )
+
+    pkinit_pin = Knob(
+        str, None,
+        sensitive=True,
+        description="The password to unlock the Kerberos KDC private key",
+        cli_aliases=['--pkinit_pin'],
+        cli_metavar='PIN',
+    )
+
+    dirsrv_cert_name = Knob(
+        str, None,
+        description="Name of the Directory Server SSL certificate to install",
+        cli_metavar='NAME',
+    )
+
+    http_cert_name = Knob(
+        str, None,
+        description="Name of the Apache Server SSL certificate to install",
+        cli_metavar='NAME',
+    )
+
+    pkinit_cert_name = Knob(
+        str, None,
+        description="Name of the Kerberos KDC SSL certificate to install",
+        cli_metavar='NAME',
+    )
+
+    ca_cert_files = Knob(
+        (list, str), None,
+        description=("File containing CA certificates for the service "
+                     "certificate files"),
+        cli_name='ca_cert_file',
+        cli_aliases=['--root-ca-file'],
+        cli_metavar='FILE',
+    )
+
+    subject = Knob(
+        str, None,
+        description="The certificate subject base (default O=<realm-name>)",
+    )
+
+    @subject.validator
+    def subject(self, value):
+        v = unicode(value, 'utf-8')
+        if any(ord(c) < 0x20 for c in v):
+            raise ValueError("must not contain control characters")
+        if '&' in v:
+            raise ValueError("must not contain an ampersand (\"&\")")
+        try:
+            dn = DN(v)
+            for rdn in dn:
+                if rdn.attr.lower() not in VALID_SUBJECT_ATTRS:
+                    raise ValueError("invalid attribute: \"%s\"" % rdn.attr)
+        except ValueError, e:
+            raise ValueError("invalid subject base format: %s" % e)
+
+    ca_signing_algorithm = Knob(
+        {'SHA1withRSA', 'SHA256withRSA', 'SHA512withRSA'}, None,
+        description="Signing algorithm of the IPA CA certificate",
+    )
+
+
+class ServerDNS(common.Installable, core.Group, core.Composite):
+    description = "DNS"
+
+    setup_dns = Knob(
+        bool, False,
+        description="configure bind with our zone",
+    )
+
+    forwarders = Knob(
+        (list, 'ip'), None,
+        description=("Add a DNS forwarder. This option can be used multiple "
+                     "times"),
+        cli_name='forwarder',
+    )
+
+    no_forwarders = Knob(
+        bool, False,
+        description="Do not add any DNS forwarders, use root servers instead",
+    )
+
+    reverse_zones = Knob(
+        (list, str), [],
+        description=("The reverse DNS zone to use. This option can be used "
+                     "multiple times"),
+        cli_name='reverse_zone',
+    )
+
+    no_reverse = Knob(
+        bool, False,
+        description="Do not create reverse DNS zone",
+    )
+
+    no_dnssec_validation = Knob(
+        bool, False,
+        description="Disable DNSSEC validation",
+    )
+
+    zonemgr = Knob(
+        str, None,
+        description=("DNS zone manager e-mail address. Defaults to "
+                     "hostmaster@DOMAIN"),
+    )
+
+    @zonemgr.validator
+    def zonemgr(self, value):
+        # validate the value first
+        try:
+            # IDNA support requires unicode
+            encoding = getattr(sys.stdin, 'encoding', None)
+            if encoding is None:
+                encoding = 'utf-8'
+            value = value.decode(encoding)
+            bindinstance.validate_zonemgr_str(value)
+        except ValueError, e:
+            # FIXME we can do this in better way
+            # https://fedorahosted.org/freeipa/ticket/4804
+            # decode to proper stderr encoding
+            stderr_encoding = getattr(sys.stderr, 'encoding', None)
+            if stderr_encoding is None:
+                stderr_encoding = 'utf-8'
+            error = unicode(e).encode(stderr_encoding)
+            raise ValueError(error)
+
+    no_host_dns = Knob(
+        bool, False,
+        description="Do not use DNS for hostname lookup during installation",
+    )
+
+    no_dns_sshfp = Knob(
+        bool, False,
+        description="Do not automatically create DNS SSHFP records",
+    )
+
+
+class Server(common.Installable, common.Interactive, core.Composite):
+    realm = Knob(
+        str, None,
+        description="realm name",
+        cli_short_name='r',
+    )
+
+    domain = Knob(
+        str, None,
+        description="domain name",
+        cli_short_name='n',
+    )
+
+    @domain.validator
+    def domain(self, value):
+        validate_domain_name(value)
+
+    dm_password = Knob(
+        str, None,
+        sensitive=True,
+        description="Directory Manager password",
+        cli_name='ds_password',
+        cli_short_name='p',
+    )
+
+    @dm_password.validator
+    def dm_password(self, value):
+        validate_dm_password(value)
+
+    master_password = Knob(
+        str, None,
+        sensitive=True,
+        deprecated=True,
+        description="kerberos master password (normally autogenerated)",
+        cli_short_name='P',
+    )
+
+    admin_password = Knob(
+        str, None,
+        sensitive=True,
+        description="admin user kerberos password",
+        cli_short_name='a',
+    )
+
+    @admin_password.validator
+    def admin_password(self, value):
+        validate_admin_password(value)
+
+    mkhomedir = Knob(
+        bool, False,
+        description="create home directories for users on their first login",
+    )
+
+    hostname = Knob(
+        str, None,
+        description="fully qualified name of server",
+    )
+
+    domain_level = Knob(
+        int, constants.MAX_DOMAIN_LEVEL,
+        description="IPA domain level",
+    )
+
+    @domain_level.validator
+    def domain_level(self, value):
+        # Check that Domain Level is within the allowed range
+        if value < constants.MIN_DOMAIN_LEVEL:
+            raise ValueError(
+                "Domain Level cannot be lower than {0}".format(
+                    constants.MIN_DOMAIN_LEVEL))
+        elif value > constants.MAX_DOMAIN_LEVEL:
+            raise ValueError(
+                "Domain Level cannot be higher than {0}".format(
+                    constants.MAX_DOMAIN_LEVEL))
+
+    ip_address = Knob(
+        (list, 'ip-local'), None,
+        description=("Master Server IP Address. This option can be used "
+                     "multiple times"),
+    )
+
+    no_ntp = Knob(
+        bool, False,
+        description="do not configure ntp",
+    )
+
+    idstart = Knob(
+        int, random.randint(1, 10000) * 200000,
+        description="The starting value for the IDs range (default random)",
+    )
+
+    idmax = Knob(
+        int,
+        description=("The max value for the IDs range (default: "
+                     "idstart+199999)"),
+    )
+
+    @idmax.default_getter
+    def idmax(self):
+        return self.idstart + 200000 - 1
+
+    no_hbac_allow = Knob(
+        bool, False,
+        description="Don't install allow_all HBAC rule",
+        cli_aliases=['--no_hbac_allow'],
+    )
+
+    no_ui_redirect = Knob(
+        bool, False,
+        description="Do not automatically redirect to the Web UI",
+    )
+
+    ssh_trust_dns = Knob(
+        bool, False,
+        description="configure OpenSSH client to trust DNS SSHFP records",
+    )
+
+    no_ssh = Knob(
+        bool, False,
+        description="do not configure OpenSSH client",
+    )
+
+    no_sshd = Knob(
+        bool, False,
+        description="do not configure OpenSSH server",
+    )
+
+    def __init__(self, **kwargs):
+        super(Server, self).__init__(**kwargs)
+
+        #Automatically disable pkinit w/ dogtag until that is supported
+        self.ca.no_pkinit = True
+
+        self.dns.dnssec_master = False
+
+    @step()
+    def main(self):
+        options = self.__get_options()
+
+        install_check(options)
+        yield
+        install(options)
+
+    @main.uninstaller
+    def main(self):
+        options = self.__get_options()
+
+        uninstall_check(options)
+        yield
+        uninstall(options)
+
+    def __get_options(self):
+        #pylint: disable=no-member
+        class ServerOptions(object):
+            pass
+
+        options = ServerOptions()
+
+        options.realm_name = self.realm
+        options.domain_name = self.domain
+        options.dm_password = self.dm_password
+        options.master_password = self.master_password
+        options.admin_password = self.admin_password
+        options.mkhomedir = self.mkhomedir
+        options.host_name = self.hostname
+        options.domainlevel = self.domain_level
+        options.ip_addresses = self.ip_address
+        options.conf_ntp = not self.no_ntp
+        options.idstart = self.idstart
+        options.idmax = self.idmax
+        options.hbac_allow = self.no_hbac_allow
+        options.ui_redirect = not self.no_ui_redirect
+        options.trust_sshfp = self.ssh_trust_dns
+        options.conf_ssh = not self.no_ssh
+        options.conf_sshd = not self.no_sshd
+        options.unattended = not self.interactive
+
+        options.external_ca = self.ca.external_ca
+        options.external_ca_type = self.ca.external_ca_type
+        options.external_cert_files = self.ca.external_cert_files
+        options.setup_pkinit = not self.ca.no_pkinit
+        options.dirsrv_cert_files = self.ca.dirsrv_cert_files
+        options.http_cert_files = self.ca.http_cert_files
+        options.pkinit_cert_files = self.ca.pkinit_cert_files
+        options.dirsrv_pin = self.ca.dirsrv_pin
+        options.http_pin = self.ca.http_pin
+        options.pkinit_pin = self.ca.pkinit_pin
+        options.dirsrv_cert_name = self.ca.dirsrv_cert_name
+        options.http_cert_name = self.ca.http_cert_name
+        options.pkinit_cert_name = self.ca.pkinit_cert_name
+        options.ca_cert_files = self.ca.ca_cert_files
+        options.subject = self.ca.subject
+        options.ca_signing_algorithm = self.ca.ca_signing_algorithm
+
+        options.setup_dns = self.dns.setup_dns
+        options.forwarders = self.dns.forwarders
+        options.no_forwarders = self.dns.no_forwarders
+        options.reverse_zones = self.dns.reverse_zones
+        options.no_reverse = self.dns.no_reverse
+        options.no_dnssec_validation = self.dns.no_dnssec_validation
+        options.zonemgr = self.dns.zonemgr
+        options.no_host_dns = self.dns.no_host_dns
+        options.create_sshfp = not self.dns.no_dns_sshfp
+
+        return options
+
+    ca = core.Component(ServerCA)
+    dns = core.Component(ServerDNS)
-- 
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