Dne 3.6.2015 v 15:02 Martin Basti napsal(a):
On 02/06/15 15:21, Jan Cholasta wrote:
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.

thank you,

1)
ipa-server-install --uninstall prints 0
...
Unconfiguring ipa_memcached
Unconfiguring ipa-otpd
0
The ipa-server-install command was successful

Fixed.



2)
ipa-server-install --setup-dns
'ServerOptions' object has no attribute 'dnssec_master'

Fixed.


3)
For record, this will be fixed in extra patch.
info messages from ldapupdate are printed to console

Could you provide the patch?


4)
+    if default is not _missing:
+        class_dict['default'] = default

Why is new _missing object needed? Isn't NoneType enough?

None is a valid value here, there needs to be a distinction between "value is not set" and "value is set to None".

Updated patches attached. Note you first have to apply my patches 436-438.

--
Jan Cholasta
>From a420ec4cff5ec72506679d995f31261571827aef Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 4 Jun 2015 11:59:22 +0000
Subject: [PATCH 1/3] install: Move private_ccache from ipaserver to ipapython

https://fedorahosted.org/freeipa/ticket/4468
---
 ipapython/ipautil.py              | 24 ++++++++++++++++++++++++
 ipaserver/install/installutils.py | 23 +----------------------
 2 files changed, 25 insertions(+), 22 deletions(-)

diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py
index abdb96d..f0c9c6b 100644
--- a/ipapython/ipautil.py
+++ b/ipapython/ipautil.py
@@ -38,6 +38,7 @@ import krbV
 import pwd
 from dns import resolver, rdatatype
 from dns.exception import DNSException
+from contextlib import contextmanager
 
 from ipapython.ipa_log_manager import *
 from ipapython import ipavalidate
@@ -1300,3 +1301,26 @@ def restore_hostname(statestore):
             run([paths.BIN_HOSTNAME, old_hostname])
         except CalledProcessError, e:
             print >>sys.stderr, "Failed to set this machine hostname back to %s: %s" % (old_hostname, str(e))
+
+
+@contextmanager
+def private_ccache(path=None):
+
+    if path is None:
+        (desc, path) = tempfile.mkstemp(prefix='krbcc')
+        os.close(desc)
+
+    original_value = os.environ.get('KRB5CCNAME', None)
+
+    os.environ['KRB5CCNAME'] = path
+
+    try:
+        yield
+    finally:
+        if original_value is not None:
+            os.environ['KRB5CCNAME'] = original_value
+        else:
+            os.environ.pop('KRB5CCNAME')
+
+        if os.path.exists(path):
+            os.remove(path)
diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index ef95b49..16b8b5d 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -42,6 +42,7 @@ import ipaplatform
 from ipapython import ipautil, sysrestore, admintool, dogtag, version
 from ipapython.admintool import ScriptError
 from ipapython.ipa_log_manager import root_logger, log_mgr
+from ipapython.ipautil import private_ccache
 from ipalib.util import validate_hostname
 from ipapython import config
 from ipalib import errors, x509
@@ -915,28 +916,6 @@ def load_pkcs12(cert_files, key_password, key_nickname, ca_cert_files,
 
     return out_file, out_password, ca_cert
 
-@contextmanager
-def private_ccache(path=None):
-
-    if path is None:
-        (desc, path) = tempfile.mkstemp(prefix='krbcc')
-        os.close(desc)
-
-    original_value = os.environ.get('KRB5CCNAME', None)
-
-    os.environ['KRB5CCNAME'] = path
-
-    try:
-        yield
-    finally:
-        if original_value is not None:
-            os.environ['KRB5CCNAME'] = original_value
-        else:
-            os.environ.pop('KRB5CCNAME')
-
-        if os.path.exists(path):
-            os.remove(path)
-
 
 @contextmanager
 def stopped_service(service, instance_name=""):
-- 
2.1.0

>From 77614ff6c0c53460bc634b99cefea873ef45d18c Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 2 Jun 2015 12:04:25 +0000
Subject: [PATCH 2/3] 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      | 255 ++++++++++++++++++++
 ipapython/install/common.py   | 115 +++++++++
 ipapython/install/core.py     | 532 ++++++++++++++++++++++++++++++++++++++++++
 ipapython/install/util.py     | 169 ++++++++++++++
 ipapython/setup.py.in         |   4 +-
 8 files changed, 1084 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 a9757a1..23c3d1a 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -884,6 +884,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..b83fd9a
--- /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 0000000..799ce50
--- /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 0000000..c313c27
--- /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 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 663a0cbaf727d5e46d049f0ff8b35b672e05a4af Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 2 Jun 2015 12:36:14 +0000
Subject: [PATCH 3/3] install: Migrate ipa-server-install to the install
 framework

https://fedorahosted.org/freeipa/ticket/4468
---
 install/tools/ipa-server-install           | 371 +-------------
 ipaserver/install/server/__init__.py       |   5 +-
 ipaserver/install/server/install.py        | 753 ++++++++++++++++++++++++-----
 ipaserver/install/server/replicainstall.py |  28 +-
 4 files changed, 658 insertions(+), 499 deletions(-)

diff --git a/install/tools/ipa-server-install b/install/tools/ipa-server-install
index c7fa5ae..9fc78b5 100755
--- a/install/tools/ipa-server-install
+++ b/install/tools/ipa-server-install
@@ -20,369 +20,18 @@
 # 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
+from ipaserver.install.server import Server
 
-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
-
-
-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)
-
-    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))
-
-    if not options.setup_dns:
-        if options.forwarders:
-            parser.error("You cannot specify a --forwarder option without the --setup-dns option")
-        if options.no_forwarders:
-            parser.error("You cannot specify a --no-forwarders option without the --setup-dns option")
-        if options.reverse_zones:
-            parser.error("You cannot specify a --reverse-zone option without the --setup-dns option")
-        if options.no_reverse:
-            parser.error("You cannot specify a --no-reverse option without the --setup-dns option")
-        if options.no_dnssec_validation:
-            parser.error("You cannot specify a --no-dnssec-validation option without the --setup-dns option")
-    elif options.forwarders and options.no_forwarders:
-        parser.error("You cannot specify a --forwarder option together with --no-forwarders")
-    elif options.reverse_zones and options.no_reverse:
-        parser.error("You cannot specify a --reverse-zone option together with --no-reverse")
-
-    if options.uninstall:
-        if (options.realm_name 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
-            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:
-            if not options.forwarders and not options.no_forwarders:
-                parser.error("You must specify at least one --forwarder option or --no-forwarders option")
-
-    # If any of the key file options are selected, all are required.
-    cert_file_req = (options.dirsrv_cert_files, options.http_cert_files)
-    cert_file_opt = (options.pkinit_cert_files,)
-    if any(cert_file_req + cert_file_opt) and not all(cert_file_req):
-        parser.error("--dirsrv-cert-file and --http-cert-file are required if "
-                     "any key file options are used.")
-
-    if options.unattended:
-        if options.dirsrv_cert_files and options.dirsrv_pin is None:
-            parser.error(
-                "You must specify --dirsrv-pin with --dirsrv-cert-file")
-        if options.http_cert_files and options.http_pin is None:
-            parser.error(
-                "You must specify --http-pin with --http-cert-file")
-        if options.pkinit_cert_files and options.pkinit_pin is None:
-            parser.error(
-                "You must specify --pkinit-pin with --pkinit-cert-file")
-
-    if options.external_cert_files and options.dirsrv_cert_files:
-        parser.error("Service certificate file options cannot be used with "
-                     "the external CA options.")
-
-    if options.external_ca:
-        if options.external_cert_files:
-            parser.error("You cannot specify --external-cert-file "
-                         "together with --external-ca")
-        if options.dirsrv_cert_files:
-            parser.error("You cannot specify service certificate file options "
-                         "together with --external-ca")
-
-    if options.external_ca_type and not options.external_ca:
-        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()
-
-    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)
 
+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,
+)
 
-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 8702167..426c5e7 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,9 +43,13 @@ except ImportError:
 
 SYSRESTORE_DIR_PATH = paths.SYSRESTORE
 
-installation_cleanup = True
-original_ccache = None
-temp_ccache = None
+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 validate_dm_password(password):
@@ -248,44 +256,15 @@ def set_subject_in_config(realm_name, dm_password, suffix, subject_base):
         conn.disconnect()
 
 
-def init_private_ccache():
-    global original_ccache
-    global temp_ccache
-
-    (desc, temp_ccache) = tempfile.mkstemp(prefix='krbcc')
-    os.close(desc)
-
-    original_ccache = os.environ.get('KRB5CCNAME')
-
-    os.environ['KRB5CCNAME'] = temp_ccache
-
-
-def destroy_private_ccache():
-    global original_ccache
-    global temp_ccache
-
-    if original_ccache is not None:
-        os.environ['KRB5CCNAME'] = original_ccache
-    else:
-        os.environ.pop('KRB5CCNAME', None)
-
-    if os.path.exists(temp_ccache):
-        os.remove(temp_ccache)
-
-
 def common_cleanup(func):
-    def decorated(*args, **kwargs):
+    def decorated(installer):
         success = False
 
         try:
-            try:
-                func(*args, **kwargs)
-            except BaseException:
-                destroy_private_ccache()
-                raise
+            func(installer)
             success = True
         except KeyboardInterrupt:
-            global ds
+            ds = installer._ds
             print "\nCleaning up..."
             if ds:
                 print "Removing configuration for %s instance" % ds.serverid
@@ -299,12 +278,11 @@ def common_cleanup(func):
                                           "manually")
             sys.exit(1)
         finally:
-            global installation_cleanup
-            if not success and installation_cleanup:
+            if not success and installer._installation_cleanup:
                 # Do a cautious clean up as we don't know what failed and
                 # what is the state of the environment
                 try:
-                    fstore.restore_file(paths.HOSTS)
+                    installer._fstore.restore_file(paths.HOSTS)
                 except:
                     pass
 
@@ -312,24 +290,17 @@ def common_cleanup(func):
 
 
 @common_cleanup
-def install_check(options):
-    global dirsrv_pkcs12_file
-    global http_pkcs12_file
-    global pkinit_pkcs12_file
-    global dirsrv_pkcs12_info
-    global http_pkcs12_info
-    global pkinit_pkcs12_info
-    global external_cert_file
-    global external_ca_file
-    global http_ca_cert
-
-    global ds
-    global installation_cleanup
-
-    # Use private ccache
-    init_private_ccache()
-
-    ds = None
+def install_check(installer):
+    options = installer
+    dirsrv_pkcs12_file = installer._dirsrv_pkcs12_file
+    http_pkcs12_file = installer._http_pkcs12_file
+    pkinit_pkcs12_file = installer._pkinit_pkcs12_file
+    dirsrv_pkcs12_info = installer._dirsrv_pkcs12_info
+    http_pkcs12_info = installer._http_pkcs12_info
+    pkinit_pkcs12_info = installer._pkinit_pkcs12_info
+    external_cert_file = installer._external_cert_file
+    external_ca_file = installer._external_ca_file
+    http_ca_cert = installer._ca_cert
 
     tasks.check_selinux_status()
 
@@ -340,27 +311,25 @@ def install_check(options):
                "manually.")
         print textwrap.fill(msg, width=79, replace_whitespace=False)
 
-    installation_cleanup = True
+    installer._installation_cleanup = True
 
     print("\nThe log file for this installation can be found in "
           "/var/log/ipaserver-install.log")
     if (not options.external_ca and not options.external_cert_files and
             is_ipa_configured()):
-        installation_cleanup = False
+        installer._installation_cleanup = False
         sys.exit("IPA server is already configured on this system.\n"
                  "If you want to reinstall the IPA server, please uninstall "
                  "it first using 'ipa-server-install --uninstall'.")
 
     client_fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
     if client_fstore.has_files():
-        installation_cleanup = False
+        installer._installation_cleanup = False
         sys.exit("IPA client is already configured on this system.\n"
                  "Please uninstall it before configuring the IPA server, "
                  "using 'ipa-client-install --uninstall'")
 
-    global fstore
     fstore = sysrestore.FileStore(SYSRESTORE_DIR_PATH)
-    global sstore
     sstore = sysrestore.StateFile(SYSRESTORE_DIR_PATH)
 
     # This will override any settings passed in on the cmdline
@@ -372,7 +341,13 @@ def install_check(options):
         if dm_password is None:
             sys.exit("Directory Manager password required")
         try:
-            options._update_loose(read_cache(dm_password))
+            cache_vars = read_cache(dm_password)
+            for name, value in cache_vars.iteritems():
+                if name not in options.__dict__:
+                    options.__dict__[name] = value
+            if cache_vars.get('external_ca', False):
+                options.external_ca = False
+                options.interactive = False
         except Exception, e:
             sys.exit("Cannot process the cache file: %s" % str(e))
 
@@ -400,29 +375,29 @@ def install_check(options):
               "management")
     if setup_kra:
         print "  * Configure a stand-alone KRA (dogtag) for key storage"
-    if options.conf_ntp:
+    if not options.no_ntp:
         print "  * Configure the Network Time Daemon (ntpd)"
     print "  * Create and configure an instance of Directory Server"
     print "  * Create and configure a Kerberos Key Distribution Center (KDC)"
     print "  * Configure Apache (httpd)"
     if options.setup_dns:
         print "  * Configure DNS (bind)"
-    if options.setup_pkinit:
+    if not options.no_pkinit:
         print "  * Configure the KDC to enable PKINIT"
-    if not options.conf_ntp:
+    if options.no_ntp:
         print ""
         print "Excluded by options:"
         print "  * Configure the Network Time Daemon (ntpd)"
-    if not options.unattended:
+    if installer.interactive:
         print ""
         print "To accept the default shown in brackets, press the Enter key."
     print ""
 
     if not options.external_cert_files:
         # Make sure the 389-ds ports are available
-        check_dirsrv(options.unattended)
+        check_dirsrv(not installer.interactive)
 
-    if options.conf_ntp:
+    if not options.no_ntp:
         try:
             ipaclient.ntpconf.check_timedate_services()
         except ipaclient.ntpconf.NTPConflictingService, e:
@@ -437,7 +412,7 @@ def install_check(options):
     if httpinstance.httpd_443_configured():
         sys.exit("Aborting installation")
 
-    if not options.setup_dns and not options.unattended:
+    if not options.setup_dns and installer.interactive:
         if ipautil.user_input("Do you want to configure integrated DNS "
                               "(BIND)?", False):
             options.setup_dns = True
@@ -460,7 +435,7 @@ def install_check(options):
         host_default = get_fqdn()
 
     try:
-        if options.unattended or options.host_name:
+        if not installer.interactive or options.host_name:
             verify_fqdn(host_default, options.no_host_dns)
             host_name = host_default
         else:
@@ -483,7 +458,7 @@ def install_check(options):
 
     if not options.domain_name:
         domain_name = read_domain_name(host_name[host_name.find(".")+1:],
-                                       options.unattended)
+                                       not installer.interactive)
         root_logger.debug("read domain_name: %s\n" % domain_name)
         try:
             validate_domain_name(domain_name)
@@ -495,7 +470,7 @@ def install_check(options):
     domain_name = domain_name.lower()
 
     if not options.realm_name:
-        realm_name = read_realm_name(domain_name, options.unattended)
+        realm_name = read_realm_name(domain_name, not installer.interactive)
         root_logger.debug("read realm_name: %s\n" % realm_name)
     else:
         realm_name = options.realm_name.upper()
@@ -592,7 +567,7 @@ def install_check(options):
         ip_addresses = dns.ip_addresses
     else:
         ip_addresses = get_server_ip_address(host_name, fstore,
-                                             options.unattended, False,
+                                             not installer.interactive, False,
                                              options.ip_addresses)
 
     print
@@ -626,7 +601,7 @@ def install_check(options):
               "Directory unless\nthe realm name of the IPA server matches "
               "its domain name.\n\n")
 
-    if not options.unattended and not user_input(
+    if installer.interactive and not user_input(
             "Continue to configure the system with these values?", False):
         sys.exit("Installation aborted")
 
@@ -636,20 +611,35 @@ def install_check(options):
     options.master_password = master_password
     options.admin_password = admin_password
     options.host_name = host_name
-    options.ip_address = ip_addresses
+    options.ip_addresses = ip_addresses
+
+    installer._fstore = fstore
+    installer._sstore = sstore
+    installer._dirsrv_pkcs12_file = dirsrv_pkcs12_file
+    installer._http_pkcs12_file = http_pkcs12_file
+    installer._pkinit_pkcs12_file = pkinit_pkcs12_file
+    installer._dirsrv_pkcs12_info = dirsrv_pkcs12_info
+    installer._http_pkcs12_info = http_pkcs12_info
+    installer._pkinit_pkcs12_info = pkinit_pkcs12_info
+    installer._external_cert_file = external_cert_file
+    installer._external_ca_file = external_ca_file
+    installer._ca_cert = http_ca_cert
 
 
 @common_cleanup
-def install(options):
-    global dirsrv_pkcs12_file
-    global http_pkcs12_file
-    global pkinit_pkcs12_file
-    global dirsrv_pkcs12_info
-    global http_pkcs12_info
-    global pkinit_pkcs12_info
-    global external_cert_file
-    global external_ca_file
-    global http_ca_cert
+def install(installer):
+    options = installer
+    fstore = installer._fstore
+    sstore = installer._sstore
+    dirsrv_pkcs12_file = installer._dirsrv_pkcs12_file
+    http_pkcs12_file = installer._http_pkcs12_file
+    pkinit_pkcs12_file = installer._pkinit_pkcs12_file
+    dirsrv_pkcs12_info = installer._dirsrv_pkcs12_info
+    http_pkcs12_info = installer._http_pkcs12_info
+    pkinit_pkcs12_info = installer._pkinit_pkcs12_info
+    external_cert_file = installer._external_cert_file
+    external_ca_file = installer._external_ca_file
+    http_ca_cert = installer._ca_cert
 
     realm_name = options.realm_name
     domain_name = options.domain_name
@@ -657,25 +647,21 @@ def install(options):
     master_password = options.master_password
     admin_password = options.admin_password
     host_name = options.host_name
-    ip_addresses = options.ip_address
+    ip_addresses = options.ip_addresses
     setup_ca = options.setup_ca
     setup_kra = options.setup_kra
 
-    global ds
-    global installation_cleanup
-
     dogtag_constants = dogtag.install_constants
 
     # Installation has started. No IPA sysrestore items are restored in case of
     # failure to enable root cause investigation
-    installation_cleanup = False
+    installer._installation_cleanup = False
 
     # Configuration for ipalib, we will bootstrap and finalize later, after
     # we are sure we have the configuration file ready.
     cfg = dict(
         context='installer',
         in_server=True,
-        debug=options.debug
     )
 
     # Create the management framework config file and finalize api
@@ -703,7 +689,7 @@ def install(options):
     # Must be readable for everyone
     os.chmod(target_fname, 0644)
 
-    if not options.unattended:
+    if installer.interactive:
         print ""
         print "The following operations may take some minutes to complete."
         print "Please wait until the prompt is returned."
@@ -731,7 +717,7 @@ def install(options):
     # Create a directory server instance
     if not options.external_cert_files:
         # Configure ntpd
-        if options.conf_ntp:
+        if not options.no_ntp:
             ipaclient.ntpconf.force_ntpd(sstore)
             ntp = ntpinstance.NTPInstance(fstore)
             if not ntp.is_configured():
@@ -740,22 +726,25 @@ def install(options):
         if options.dirsrv_cert_files:
             ds = dsinstance.DsInstance(fstore=fstore,
                                        domainlevel=options.domainlevel)
+            installer._ds = ds
             ds.create_instance(realm_name, host_name, domain_name,
                                dm_password, dirsrv_pkcs12_info,
                                idstart=options.idstart, idmax=options.idmax,
                                subject_base=options.subject,
-                               hbac_allow=not options.hbac_allow)
+                               hbac_allow=not options.no_hbac_allow)
         else:
             ds = dsinstance.DsInstance(fstore=fstore,
                                        domainlevel=options.domainlevel)
+            installer._ds = ds
             ds.create_instance(realm_name, host_name, domain_name,
                                dm_password,
                                idstart=options.idstart, idmax=options.idmax,
                                subject_base=options.subject,
-                               hbac_allow=not options.hbac_allow)
+                               hbac_allow=not options.no_hbac_allow)
     else:
         ds = dsinstance.DsInstance(fstore=fstore,
                                    domainlevel=options.domainlevel)
+        installer._ds = ds
         ds.init_info(
             realm_name, host_name, domain_name, dm_password,
             options.subject, 1101, 1100, None)
@@ -769,10 +758,10 @@ def install(options):
             options.dm_password = dm_password
             options.admin_password = admin_password
             options.host_name = host_name
-            options.unattended = True
             options.forwarders = dns.dns_forwarders
             options.reverse_zones = dns.reverse_zones
-            write_cache(vars(options))
+            cache_vars = {n: getattr(options, n) for o, n in installer.knobs()}
+            write_cache(cache_vars)
 
         ca.install_step_0(False, None, options)
 
@@ -795,13 +784,13 @@ def install(options):
     if options.pkinit_cert_files:
         krb.create_instance(realm_name, host_name, domain_name,
                             dm_password, master_password,
-                            setup_pkinit=options.setup_pkinit,
+                            setup_pkinit=not options.no_pkinit,
                             pkcs12_info=pkinit_pkcs12_info,
                             subject_base=options.subject)
     else:
         krb.create_instance(realm_name, host_name, domain_name,
                             dm_password, master_password,
-                            setup_pkinit=options.setup_pkinit,
+                            setup_pkinit=not options.no_pkinit,
                             subject_base=options.subject)
 
     # The DS instance is created before the keytab, add the SSL cert we
@@ -822,12 +811,13 @@ def install(options):
         http.create_instance(
             realm_name, host_name, domain_name, dm_password,
             pkcs12_info=http_pkcs12_info, subject_base=options.subject,
-            auto_redirect=options.ui_redirect,
+            auto_redirect=not options.no_ui_redirect,
             ca_is_configured=setup_ca)
     else:
         http.create_instance(
             realm_name, host_name, domain_name, dm_password,
-            subject_base=options.subject, auto_redirect=options.ui_redirect,
+            subject_base=options.subject,
+            auto_redirect=not options.no_ui_redirect,
             ca_is_configured=setup_ca)
     tasks.restore_context(paths.CACHE_IPA_SESSIONS)
 
@@ -862,7 +852,7 @@ def install(options):
         # Create a BIND instance
         bind = bindinstance.BindInstance(fstore, dm_password)
         bind.setup(host_name, ip_addresses, realm_name,
-                   domain_name, (), options.conf_ntp, (),
+                   domain_name, (), not options.no_ntp, (),
                    zonemgr=options.zonemgr, ca_configured=setup_ca,
                    no_dnssec_validation=options.no_dnssec_validation)
         bind.create_sample_bind_zone()
@@ -882,13 +872,13 @@ def install(options):
         args = [paths.IPA_CLIENT_INSTALL, "--on-master", "--unattended",
                 "--domain", domain_name, "--server", host_name,
                 "--realm", realm_name, "--hostname", host_name]
-        if not options.create_sshfp:
+        if options.no_dns_sshfp:
             args.append("--no-dns-sshfp")
-        if options.trust_sshfp:
+        if options.ssh_trust_dns:
             args.append("--ssh-trust-dns")
-        if not options.conf_ssh:
+        if options.no_ssh:
             args.append("--no-ssh")
-        if not options.conf_sshd:
+        if options.no_sshd:
             args.append("--no-sshd")
         if options.mkhomedir:
             args.append("--mkhomedir")
@@ -916,7 +906,7 @@ def install(options):
     print "\t\t  * 88, 464: kerberos"
     if options.setup_dns:
         print "\t\t  * 53: bind"
-    if options.conf_ntp:
+    if not options.no_ntp:
         print "\t\t  * 123: ntp"
     print ""
     print("\t2. You can now obtain a kerberos ticket using the command: "
@@ -947,19 +937,10 @@ def install(options):
     if ipautil.file_exists(paths.ROOT_IPA_CACHE):
         os.remove(paths.ROOT_IPA_CACHE)
 
-    # Use private ccache
-    destroy_private_ccache()
-
 
 @common_cleanup
-def uninstall_check(options):
-    global ds
-    global installation_cleanup
-
-    # Use private ccache
-    init_private_ccache()
-
-    ds = None
+def uninstall_check(installer):
+    options = installer
 
     tasks.check_selinux_status()
 
@@ -970,11 +951,9 @@ def uninstall_check(options):
                "manually.")
         print textwrap.fill(msg, width=79, replace_whitespace=False)
 
-    installation_cleanup = False
+    installer._installation_cleanup = False
 
-    global fstore
     fstore = sysrestore.FileStore(SYSRESTORE_DIR_PATH)
-    global sstore
     sstore = sysrestore.StateFile(SYSRESTORE_DIR_PATH)
 
     # Configuration for ipalib, we will bootstrap and finalize later, after
@@ -982,7 +961,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
@@ -990,7 +968,7 @@ def uninstall_check(options):
     api.bootstrap(**cfg)
     api.finalize()
 
-    if not options.unattended:
+    if installer.interactive:
         print("\nThis is a NON REVERSIBLE operation and will delete all data "
               "and configuration!\n")
         if not user_input("Are you sure you want to continue with the "
@@ -1037,18 +1015,22 @@ def uninstall_check(options):
             cmd = "$ ipa-replica-manage del %s\n" % api.env.host
             print textwrap.fill(msg, width=80, replace_whitespace=False)
             print cmd
-            if not (options.unattended or user_input("Are you sure you "
-                                                     "want to continue "
-                                                     "with the uninstall "
-                                                     "procedure?",
-                                                     False)):
+            if (installer.interactive and
+                not user_input("Are you sure you want to continue with the "
+                               "uninstall procedure?", False)):
                 print ""
                 print "Aborting uninstall operation."
                 sys.exit(1)
 
+    installer._fstore = fstore
+    installer._sstore = sstore
+
 
 @common_cleanup
-def uninstall(options):
+def uninstall(installer):
+    fstore = installer._fstore
+    sstore = installer._sstore
+
     rv = 0
 
     print "Shutting down all IPA services"
@@ -1151,7 +1133,512 @@ def uninstall(options):
                           ' # getcert stop-tracking -i <request_id>\n'
                           'for each id in: %s' % ', '.join(ids))
 
-    # Use private ccache
-    destroy_private_ccache()
-
     sys.exit(rv)
+
+
+class ServerCA(common.Installable, core.Group, core.Composite):
+    description = "certificate system"
+
+    setup_ca = Knob(
+        bool, False,
+        initializable=False,
+        description="configure a dogtag CA",
+    )
+
+    setup_kra = Knob(
+        bool, False,
+        initializable=False,
+        description="configure a dogtag KRA",
+    )
+
+    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",
+    )
+
+    dnssec_master = Knob(
+        bool, False,
+        initializable=False,
+        description="Setup server to be DNSSEC key master",
+    )
+
+    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_name = Knob(
+        str, None,
+        description="realm name",
+        cli_name='realm',
+        cli_short_name='r',
+    )
+
+    domain_name = Knob(
+        str, None,
+        description="domain name",
+        cli_name='domain',
+        cli_short_name='n',
+    )
+
+    @domain_name.validator
+    def domain_name(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",
+    )
+
+    host_name = Knob(
+        str, None,
+        description="fully qualified name of server",
+        cli_name='hostname',
+    )
+
+    domainlevel = Knob(
+        int, constants.MAX_DOMAIN_LEVEL,
+        description="IPA domain level",
+        cli_name='domain_level',
+    )
+
+    @domainlevel.validator
+    def domainlevel(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_addresses = Knob(
+        (list, 'ip-local'), None,
+        description=("Master Server IP Address. This option can be used "
+                     "multiple times"),
+        cli_name='ip_address',
+    )
+
+    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)
+
+        self._installation_cleanup = True
+        self._ds = None
+
+        self._dirsrv_pkcs12_file = None
+        self._http_pkcs12_file = None
+        self._pkinit_pkcs12_file = None
+        self._dirsrv_pkcs12_info = None
+        self._http_pkcs12_info = None
+        self._pkinit_pkcs12_info = None
+        self._external_cert_file = None
+        self._external_ca_file = None
+        self._ca_cert = None
+
+        #pylint: disable=no-member
+
+        if not self.dns.setup_dns:
+            if self.dns.forwarders:
+                raise RuntimeError(
+                    "You cannot specify a --forwarder option without the "
+                    "--setup-dns option")
+            if self.dns.no_forwarders:
+                raise RuntimeError(
+                    "You cannot specify a --no-forwarders option without the "
+                    "--setup-dns option")
+            if self.dns.reverse_zones:
+                raise RuntimeError(
+                    "You cannot specify a --reverse-zone option without the "
+                    "--setup-dns option")
+            if self.dns.no_reverse:
+                raise RuntimeError(
+                    "You cannot specify a --no-reverse option without the "
+                    "--setup-dns option")
+            if self.dns.no_dnssec_validation:
+                raise RuntimeError(
+                    "You cannot specify a --no-dnssec-validation option "
+                    "without the --setup-dns option")
+        elif self.dns.forwarders and self.dns.no_forwarders:
+            raise RuntimeError(
+                "You cannot specify a --forwarder option together with "
+                "--no-forwarders")
+        elif self.dns.reverse_zones and self.dns.no_reverse:
+            raise RuntimeError(
+                "You cannot specify a --reverse-zone option together with "
+                "--no-reverse")
+
+        if self.uninstalling:
+            if (self.realm_name or self.admin_password or
+                    self.master_password):
+                raise RuntimeError(
+                    "In uninstall mode, -a, -r and -P options are not allowed")
+        elif not self.interactive:
+            if (not self.realm_name or not self.dm_password or
+                    not self.admin_password):
+                raise RuntimeError(
+                    "In unattended mode you need to provide at least -r, -p "
+                    "and -a options")
+            if self.dns.setup_dns:
+                if not self.dns.forwarders and not self.dns.no_forwarders:
+                    raise RuntimeError(
+                        "You must specify at least one --forwarder option or "
+                        "--no-forwarders option")
+
+        # If any of the key file options are selected, all are required.
+        cert_file_req = (self.ca.dirsrv_cert_files, self.ca.http_cert_files)
+        cert_file_opt = (self.ca.pkinit_cert_files,)
+        if any(cert_file_req + cert_file_opt) and not all(cert_file_req):
+            raise RuntimeError(
+                "--dirsrv-cert-file and --http-cert-file are required if any "
+                "key file options are used.")
+
+        if not self.interactive:
+            if self.ca.dirsrv_cert_files and self.ca.dirsrv_pin is None:
+                raise RuntimeError(
+                    "You must specify --dirsrv-pin with --dirsrv-cert-file")
+            if self.ca.http_cert_files and self.ca.http_pin is None:
+                raise RuntimeError(
+                    "You must specify --http-pin with --http-cert-file")
+            if self.ca.pkinit_cert_files and self.ca.pkinit_pin is None:
+                raise RuntimeError(
+                    "You must specify --pkinit-pin with --pkinit-cert-file")
+
+        if self.ca.external_cert_files and self.ca.dirsrv_cert_files:
+            raise RuntimeError(
+                "Service certificate file options cannot be used with the "
+                "external CA options.")
+
+        if self.ca.external_ca_type and not self.ca.external_ca:
+            raise RuntimeError(
+                "You cannot specify --external-ca-type without --external-ca")
+
+        if self.idmax < self.idstart:
+            raise RuntimeError(
+                "idmax (%s) cannot be smaller than idstart (%s)" %
+                (self.idmax, self.idstart))
+
+        # Automatically disable pkinit w/ dogtag until that is supported
+        self.ca.no_pkinit = True
+
+        self.setup_ca = self.ca.setup_ca
+        self.setup_kra = self.ca.setup_kra
+        self.external_ca = self.ca.external_ca
+        self.external_ca_type = self.ca.external_ca_type
+        self.external_cert_files = self.ca.external_cert_files
+        self.no_pkinit = self.ca.no_pkinit
+        self.dirsrv_cert_files = self.ca.dirsrv_cert_files
+        self.http_cert_files = self.ca.http_cert_files
+        self.pkinit_cert_files = self.ca.pkinit_cert_files
+        self.dirsrv_pin = self.ca.dirsrv_pin
+        self.http_pin = self.ca.http_pin
+        self.pkinit_pin = self.ca.pkinit_pin
+        self.dirsrv_cert_name = self.ca.dirsrv_cert_name
+        self.http_cert_name = self.ca.http_cert_name
+        self.pkinit_cert_name = self.ca.pkinit_cert_name
+        self.ca_cert_files = self.ca.ca_cert_files
+        self.subject = self.ca.subject
+        self.ca_signing_algorithm = self.ca.ca_signing_algorithm
+
+        self.setup_dns = self.dns.setup_dns
+        self.forwarders = self.dns.forwarders
+        self.no_forwarders = self.dns.no_forwarders
+        self.reverse_zones = self.dns.reverse_zones
+        self.no_reverse = self.dns.no_reverse
+        self.no_dnssec_validation = self.dns.no_dnssec_validation
+        self.dnssec_master = self.dns.dnssec_master
+        self.zonemgr = self.dns.zonemgr
+        self.no_host_dns = self.dns.no_host_dns
+        self.no_dns_sshfp = self.dns.no_dns_sshfp
+
+    @step()
+    def main(self):
+        install_check(self)
+        yield
+        install(self)
+
+    @main.uninstaller
+    def main(self):
+        uninstall_check(self)
+        yield
+        uninstall(self)
+
+    ca = core.Component(ServerCA)
+    dns = core.Component(ServerDNS)
diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py
index d648818..149d6b4 100644
--- a/ipaserver/install/server/replicainstall.py
+++ b/ipaserver/install/server/replicainstall.py
@@ -10,6 +10,7 @@ import os
 import shutil
 import socket
 import sys
+import tempfile
 
 from ipapython import dogtag, ipautil, sysrestore
 from ipapython.dn import DN
@@ -25,8 +26,6 @@ from ipaserver.install import (
 from ipaserver.install.installutils import create_replica_config
 from ipaserver.install.replication import (
     ReplicationManager, replica_conn_check)
-from ipaserver.install.server.install import (
-    init_private_ccache, destroy_private_ccache)
 
 DIRMAN_DN = DN(('cn', 'directory manager'))
 REPLICA_INFO_TOP_DIR = None
@@ -286,6 +285,31 @@ def remove_replica_info_dir():
         pass
 
 
+def init_private_ccache():
+    global original_ccache
+    global temp_ccache
+
+    (desc, temp_ccache) = tempfile.mkstemp(prefix='krbcc')
+    os.close(desc)
+
+    original_ccache = os.environ.get('KRB5CCNAME')
+
+    os.environ['KRB5CCNAME'] = temp_ccache
+
+
+def destroy_private_ccache():
+    global original_ccache
+    global temp_ccache
+
+    if original_ccache is not None:
+        os.environ['KRB5CCNAME'] = original_ccache
+    else:
+        os.environ.pop('KRB5CCNAME', None)
+
+    if os.path.exists(temp_ccache):
+        os.remove(temp_ccache)
+
+
 def common_cleanup(func):
     def decorated(*args, **kwargs):
         try:
-- 
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