Hi,

the attached patches fix <https://fedorahosted.org/freeipa/ticket/3090> and <https://fedorahosted.org/freeipa/ticket/5073>.


Honza

--
Jan Cholasta
>From f7d33fa9f10da20460fb3d1c0a62c96742edab29 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Wed, 24 Jun 2015 15:14:54 +0000
Subject: [PATCH 01/13] plugable: Move plugin base class and override logic to
 API

Each API object now maintains its own view of registered plugins. This change
removes the need to register plugin base classes.

This reverts commit 2db741e847c60d712dbc8ee1cd65a978a78eb312.

https://fedorahosted.org/freeipa/ticket/3090
https://fedorahosted.org/freeipa/ticket/5073
---
 ipalib/backend.py                     |   3 -
 ipalib/frontend.py                    |   9 +-
 ipalib/plugable.py                    | 273 +++++++++++++++-------------------
 ipaserver/advise/base.py              |   5 +-
 ipatests/test_ipalib/test_plugable.py | 118 +++------------
 5 files changed, 144 insertions(+), 264 deletions(-)

diff --git a/ipalib/backend.py b/ipalib/backend.py
index fcbbd25..0f381cb 100644
--- a/ipalib/backend.py
+++ b/ipalib/backend.py
@@ -27,10 +27,7 @@ import os
 from errors import PublicError, InternalError, CommandError
 from request import context, Connection, destroy_context
 
-register = plugable.Registry()
 
-
-@register.base()
 class Backend(plugable.Plugin):
     """
     Base class for all backend plugins.
diff --git a/ipalib/frontend.py b/ipalib/frontend.py
index 19190c3..0b42cb6 100644
--- a/ipalib/frontend.py
+++ b/ipalib/frontend.py
@@ -27,7 +27,7 @@ from distutils import version
 from ipapython.version import API_VERSION
 from ipapython.ipa_log_manager import root_logger
 from base import NameSpace
-from plugable import Plugin, Registry, is_production_mode
+from plugable import Plugin, is_production_mode
 from parameters import create_param, Param, Str, Flag, Password
 from output import Output, Entry, ListOfEntries
 from text import _
@@ -40,9 +40,6 @@ from textwrap import wrap
 
 RULE_FLAG = 'validation_rule'
 
-register = Registry()
-
-
 def rule(obj):
     assert not hasattr(obj, RULE_FLAG)
     setattr(obj, RULE_FLAG, True)
@@ -369,7 +366,6 @@ class HasParam(Plugin):
         setattr(self, name, namespace)
 
 
-@register.base()
 class Command(HasParam):
     """
     A public IPA atomic operation.
@@ -1124,7 +1120,6 @@ class Local(Command):
         return self.forward(*args, **options)
 
 
-@register.base()
 class Object(HasParam):
     finalize_early = False
 
@@ -1283,7 +1278,6 @@ class Attribute(Plugin):
         super(Attribute, self)._on_finalize()
 
 
-@register.base()
 class Method(Attribute, Command):
     """
     A command with an associated object.
@@ -1370,7 +1364,6 @@ class Method(Attribute, Command):
             yield param
 
 
-@register.base()
 class Updater(Plugin):
     """
     An LDAP update with an associated object (always update).
diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index 4c42e1e..ad662e5 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -35,6 +35,7 @@ import subprocess
 import optparse
 import errors
 import textwrap
+import collections
 
 from config import Env
 import util
@@ -74,94 +75,13 @@ class Registry(object):
     For forward compatibility, make sure that the module-level instance of
     this object is named "register".
     """
-
-    __allowed = {}
-    __registered = set()
-
-    def base(self):
-        def decorator(base):
-            if not inspect.isclass(base):
-                raise TypeError('plugin base must be a class; got %r' % base)
-
-            if base in self.__allowed:
-                raise errors.PluginDuplicateError(plugin=base)
-
-            self.__allowed[base] = {}
-
-            return base
-
-        return decorator
-
-    def __findbases(self, klass):
-        """
-        Iterates through allowed bases that ``klass`` is a subclass of.
-
-        Raises `errors.PluginSubclassError` if ``klass`` is not a subclass of
-        any allowed base.
-
-        :param klass: The plugin class to find bases for.
-        """
-        found = False
-        for (base, sub_d) in self.__allowed.iteritems():
-            if issubclass(klass, base):
-                found = True
-                yield (base, sub_d)
-        if not found:
-            raise errors.PluginSubclassError(
-                plugin=klass, bases=self.__allowed.keys()
-            )
-
-    def __call__(self, override=False):
-        def decorator(klass):
-            if not inspect.isclass(klass):
-                raise TypeError('plugin must be a class; got %r' % klass)
-
-            # Raise DuplicateError if this exact class was already registered:
-            if klass in self.__registered:
-                raise errors.PluginDuplicateError(plugin=klass)
-
-            # Find the base class or raise SubclassError:
-            for (base, sub_d) in self.__findbases(klass):
-                # Check override:
-                if klass.__name__ in sub_d:
-                    if not override:
-                        # Must use override=True to override:
-                        raise errors.PluginOverrideError(
-                            base=base.__name__,
-                            name=klass.__name__,
-                            plugin=klass,
-                        )
-                else:
-                    if override:
-                        # There was nothing already registered to override:
-                        raise errors.PluginMissingOverrideError(
-                            base=base.__name__,
-                            name=klass.__name__,
-                            plugin=klass,
-                        )
-
-                # The plugin is okay, add to sub_d:
-                sub_d[klass.__name__] = klass
-
-            # The plugin is okay, add to __registered:
-            self.__registered.add(klass)
-
-            return klass
+    def __call__(self):
+        def decorator(cls):
+            API.register(cls)
+            return cls
 
         return decorator
 
-    def __base_iter(self, *allowed):
-        for base in allowed:
-            sub_d = self.__allowed[base]
-            subclasses = set(sub_d.itervalues())
-            yield (base, subclasses)
-
-    def iter(self, *allowed):
-        for base in allowed:
-            if base not in self.__allowed:
-                raise TypeError("unknown plugin base %r" % base)
-        return self.__base_iter(*allowed)
-
 
 class SetProxy(ReadOnly):
     """
@@ -441,29 +361,61 @@ class Plugin(ReadOnly):
         )
 
 
+class Registrar(collections.Mapping):
+    """
+    Collects plugin classes as they are registered.
+
+    The Registrar does not instantiate plugins... it only implements the
+    override logic and stores the plugins in a namespace per allowed base
+    class.
+
+    The plugins are instantiated when `API.finalize()` is called.
+    """
+    def __init__(self):
+        self.__registry = collections.OrderedDict()
+
+    def __call__(self, klass, override=False):
+        """
+        Register the plugin ``klass``.
+
+        :param klass: A subclass of `Plugin` to attempt to register.
+        :param override: If true, override an already registered plugin.
+        """
+        if not inspect.isclass(klass):
+            raise TypeError('plugin must be a class; got %r' % klass)
+
+        # Raise DuplicateError if this exact class was already registered:
+        if klass in self.__registry:
+            raise errors.PluginDuplicateError(plugin=klass)
+
+        # The plugin is okay, add to __registry:
+        self.__registry[klass] = dict(override=override)
+
+    def __getitem__(self, key):
+        return self.__registry[key]
+
+    def __iter__(self):
+        return iter(self.__registry)
+
+    def __len__(self):
+        return len(self.__registry)
+
+
 class API(DictProxy):
     """
     Dynamic API object through which `Plugin` instances are accessed.
     """
 
+    register = Registrar()
+
     def __init__(self, allowed, packages):
-        self.__allowed = allowed
+        self.__plugins = {base: {} for base in allowed}
         self.packages = packages
         self.__d = dict()
         self.__done = set()
-        self.__registry = Registry()
         self.env = Env()
         super(API, self).__init__(self.__d)
 
-    def register(self, klass, override=False):
-        """
-        Register the plugin ``klass``.
-
-        :param klass: A subclass of `Plugin` to attempt to register.
-        :param override: If true, override an already registered plugin.
-        """
-        self.__registry(override)(klass)
-
     def __doing(self, name):
         if name in self.__done:
             raise StandardError(
@@ -638,6 +590,8 @@ class API(DictProxy):
             return
         for package in self.packages:
             self.import_plugins(package)
+        for klass, kwargs in self.register.iteritems():
+            self.add_plugin(klass, **kwargs)
 
     # FIXME: This method has no unit test
     def import_plugins(self, package):
@@ -686,6 +640,51 @@ class API(DictProxy):
                     self.log.error('could not load plugin module %r\n%s', pyfile, traceback.format_exc())
                 raise
 
+    def add_plugin(self, klass, override=False):
+        """
+        Add the plugin ``klass``.
+
+        :param klass: A subclass of `Plugin` to attempt to add.
+        :param override: If true, override an already added plugin.
+        """
+        if not inspect.isclass(klass):
+            raise TypeError('plugin must be a class; got %r' % klass)
+
+        # Find the base class or raise SubclassError:
+        found = False
+        for (base, sub_d) in self.__plugins.iteritems():
+            if not issubclass(klass, base):
+                continue
+
+            found = True
+
+            # Check override:
+            if klass.__name__ in sub_d:
+                if not override:
+                    # Must use override=True to override:
+                    raise errors.PluginOverrideError(
+                        base=base.__name__,
+                        name=klass.__name__,
+                        plugin=klass,
+                    )
+            else:
+                if override:
+                    # There was nothing already registered to override:
+                    raise errors.PluginMissingOverrideError(
+                        base=base.__name__,
+                        name=klass.__name__,
+                        plugin=klass,
+                    )
+
+            # The plugin is okay, add to sub_d:
+            sub_d[klass.__name__] = klass
+
+        if not found:
+            raise errors.PluginSubclassError(
+                plugin=klass,
+                bases=self.__plugins.keys(),
+            )
+
     def finalize(self):
         """
         Finalize the registration, instantiate the plugins.
@@ -696,56 +695,25 @@ class API(DictProxy):
         self.__doing('finalize')
         self.__do_if_not_done('load_plugins')
 
-        class PluginInstance(object):
-            """
-            Represents a plugin instance.
-            """
-
-            i = 0
-
-            def __init__(self, klass):
-                self.created = self.next()
-                self.klass = klass
-                self.instance = klass()
-                self.bases = []
-
-            @classmethod
-            def next(cls):
-                cls.i += 1
-                return cls.i
-
-        class PluginInfo(ReadOnly):
-            def __init__(self, p):
-                assert isinstance(p, PluginInstance)
-                self.created = p.created
-                self.name = p.klass.__name__
-                self.module = str(p.klass.__module__)
-                self.plugin = '%s.%s' % (self.module, self.name)
-                self.bases = tuple(b.__name__ for b in p.bases)
-                if not is_production_mode(self):
-                    lock(self)
-
+        production_mode = is_production_mode(self)
         plugins = {}
-        tofinalize = set()
-        def plugin_iter(base, subclasses):
-            for klass in subclasses:
-                assert issubclass(klass, base)
-                if klass not in plugins:
-                    plugins[klass] = PluginInstance(klass)
-                p = plugins[klass]
-                if not is_production_mode(self):
-                    assert base not in p.bases
-                p.bases.append(base)
-                if klass.finalize_early or not self.env.plugins_on_demand:
-                    tofinalize.add(p)
-                yield p.instance
+        plugin_info = {}
 
-        production_mode = is_production_mode(self)
-        for base, subclasses in self.__registry.iter(*self.__allowed):
+        for base, sub_d in self.__plugins.iteritems():
             name = base.__name__
-            namespace = NameSpace(
-                plugin_iter(base, subclasses)
-            )
+
+            members = []
+            for klass in sub_d.itervalues():
+                try:
+                    instance = plugins[klass]
+                except KeyError:
+                    instance = plugins[klass] = klass()
+                members.append(instance)
+                plugin_info.setdefault(
+                    '%s.%s' % (klass.__module__, klass.__name__),
+                    []).append(name)
+
+            namespace = NameSpace(members)
             if not production_mode:
                 assert not (
                     name in self.__d or hasattr(self, name)
@@ -753,19 +721,20 @@ class API(DictProxy):
             self.__d[name] = namespace
             object.__setattr__(self, name, namespace)
 
-        for p in plugins.itervalues():
-            p.instance.set_api(self)
-            if not production_mode:
-                assert p.instance.api is self
+        for instance in plugins.itervalues():
+            instance.set_api(self)
 
-        for p in tofinalize:
-            p.instance.ensure_finalized()
+        for klass, instance in plugins.iteritems():
             if not production_mode:
-                assert islocked(p.instance) is True
+                assert instance.api is self
+            if klass.finalize_early or not self.env.plugins_on_demand:
+                instance.ensure_finalized()
+                if not production_mode:
+                    assert islocked(instance)
+
         object.__setattr__(self, '_API__finalized', True)
-        tuple(PluginInfo(p) for p in plugins.itervalues())
         object.__setattr__(self, 'plugins',
-            tuple(PluginInfo(p) for p in plugins.itervalues())
+            tuple((k, tuple(v)) for k, v in plugin_info.iteritems())
         )
 
 
diff --git a/ipaserver/advise/base.py b/ipaserver/advise/base.py
index 0c68358..ab8323c 100644
--- a/ipaserver/advise/base.py
+++ b/ipaserver/advise/base.py
@@ -19,14 +19,12 @@
 
 import os
 from ipalib import api
-from ipalib.plugable import Plugin, Registry, API
+from ipalib.plugable import Plugin, API
 from ipalib.errors import ValidationError
 from ipapython import admintool
 from textwrap import wrap
 from ipapython.ipa_log_manager import log_mgr
 
-register = Registry()
-
 
 """
 To add configuration instructions for a new use case, define a new class that
@@ -97,7 +95,6 @@ class _AdviceOutput(object):
         self.content.append(line)
 
 
-@register.base()
 class Advice(Plugin):
     """
     Base class for advices, plugins for ipa-advise.
diff --git a/ipatests/test_ipalib/test_plugable.py b/ipatests/test_ipalib/test_plugable.py
index ad1f79f..2a6f8aa 100644
--- a/ipatests/test_ipalib/test_plugable.py
+++ b/ipatests/test_ipalib/test_plugable.py
@@ -287,9 +287,9 @@ class test_Plugin(ClassChecker):
         assert e.argv == (paths.BIN_FALSE,)
 
 
-def test_Registry():
+def test_Registrar():
     """
-    Test the `ipalib.plugable.Registry` class
+    Test the `ipalib.plugable.Registrar` class
     """
     class Base1(object):
         pass
@@ -304,47 +304,8 @@ def test_Registry():
     class plugin3(Base3):
         pass
 
-    # Test creation of Registry:
-    register = plugable.Registry()
-    def b(klass):
-        register.base()(klass)
-    def r(klass, override=False):
-        register(override=override)(klass)
-
-    # Check that TypeError is raised trying to register base that isn't
-    # a class:
-    p = Base1()
-    e = raises(TypeError, b, p)
-    assert str(e) == 'plugin base must be a class; got %r' % p
-
-    # Check that base registration works
-    b(Base1)
-    i = tuple(register.iter(Base1))
-    assert len(i) == 1
-    assert i[0][0] is Base1
-    assert not i[0][1]
-
-    # Check that DuplicateError is raised trying to register exact class
-    # again:
-    e = raises(errors.PluginDuplicateError, b, Base1)
-    assert e.plugin is Base1
-
-    # Test that another base can be registered:
-    b(Base2)
-    i = tuple(register.iter(Base2))
-    assert len(i) == 1
-    assert i[0][0] is Base2
-    assert not i[0][1]
-
-    # Test iter:
-    i = tuple(register.iter(Base1, Base2))
-    assert len(i) == 2
-    assert i[0][0] is Base1
-    assert not i[0][1]
-    assert i[1][0] is Base2
-    assert not i[1][1]
-    e = raises(TypeError, register.iter, Base1, Base2, Base3)
-    assert str(e) == 'unknown plugin base %r' % Base3
+    # Test creation of Registrar:
+    r = plugable.Registrar()
 
     # Check that TypeError is raised trying to register something that isn't
     # a class:
@@ -352,59 +313,33 @@ def test_Registry():
     e = raises(TypeError, r, p)
     assert str(e) == 'plugin must be a class; got %r' % p
 
-    # Check that SubclassError is raised trying to register a class that is
-    # not a subclass of an allowed base:
-    e = raises(errors.PluginSubclassError, r, plugin3)
-    assert e.plugin is plugin3
-
     # Check that registration works
     r(plugin1)
-    i = tuple(register.iter(Base1))
-    assert len(i) == 1
-    assert i[0][0] is Base1
-    assert i[0][1] == {plugin1}
+    assert len(r) == 1
+    assert plugin1 in r
+    assert r[plugin1] == dict(override=False)
 
     # Check that DuplicateError is raised trying to register exact class
     # again:
     e = raises(errors.PluginDuplicateError, r, plugin1)
     assert e.plugin is plugin1
 
-    # Check that OverrideError is raised trying to register class with same
-    # name and same base:
+    # Check that overriding works
     orig1 = plugin1
     class base1_extended(Base1):
         pass
     class plugin1(base1_extended):  # pylint: disable=function-redefined
         pass
-    e = raises(errors.PluginOverrideError, r, plugin1)
-    assert e.base == 'Base1'
-    assert e.name == 'plugin1'
-    assert e.plugin is plugin1
-
-    # Check that overriding works
     r(plugin1, override=True)
-    i = tuple(register.iter(Base1))
-    assert len(i) == 1
-    assert i[0][0] is Base1
-    assert i[0][1] == {plugin1}
-
-    # Check that MissingOverrideError is raised trying to override a name
-    # not yet registerd:
-    e = raises(errors.PluginMissingOverrideError, r, plugin2, override=True)
-    assert e.base == 'Base2'
-    assert e.name == 'plugin2'
-    assert e.plugin is plugin2
+    assert len(r) == 2
+    assert plugin1 in r
+    assert r[plugin1] == dict(override=True)
 
     # Test that another plugin can be registered:
-    i = tuple(register.iter(Base2))
-    assert len(i) == 1
-    assert i[0][0] is Base2
-    assert not i[0][1]
     r(plugin2)
-    i = tuple(register.iter(Base2))
-    assert len(i) == 1
-    assert i[0][0] is Base2
-    assert i[0][1] == {plugin2}
+    assert len(r) == 3
+    assert plugin2 in r
+    assert r[plugin2] == dict(override=False)
 
     # Setup to test more registration:
     class plugin1a(Base1):
@@ -423,14 +358,6 @@ def test_Registry():
         pass
     r(plugin2b)
 
-    # Again test iter:
-    i = tuple(register.iter(Base1, Base2))
-    assert len(i) == 2
-    assert i[0][0] is Base1
-    assert i[0][1] == {plugin1, plugin1a, plugin1b}
-    assert i[1][0] is Base2
-    assert i[1][1] == {plugin2, plugin2a, plugin2b}
-
 
 class test_API(ClassChecker):
     """
@@ -445,15 +372,11 @@ class test_API(ClassChecker):
         """
         assert issubclass(plugable.API, plugable.ReadOnly)
 
-        register = plugable.Registry()
-
         # Setup the test bases, create the API:
-        @register.base()
         class base0(plugable.Plugin):
             def method(self, n):
                 return n
 
-        @register.base()
         class base1(plugable.Plugin):
             def method(self, n):
                 return n + 1
@@ -461,30 +384,31 @@ class test_API(ClassChecker):
         api = plugable.API([base0, base1], [])
         api.env.mode = 'unit_test'
         api.env.in_tree = True
+        r = api.register
 
-        @register()
         class base0_plugin0(base0):
             pass
+        r(base0_plugin0)
 
-        @register()
         class base0_plugin1(base0):
             pass
+        r(base0_plugin1)
 
-        @register()
         class base0_plugin2(base0):
             pass
+        r(base0_plugin2)
 
-        @register()
         class base1_plugin0(base1):
             pass
+        r(base1_plugin0)
 
-        @register()
         class base1_plugin1(base1):
             pass
+        r(base1_plugin1)
 
-        @register()
         class base1_plugin2(base1):
             pass
+        r(base1_plugin2)
 
         # Test API instance:
         assert api.isdone('bootstrap') is False
-- 
2.1.0

>From 7cfe4da74f37143af1e70c1966545c505c0f89b3 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 22 Jun 2015 09:59:33 +0000
Subject: [PATCH 02/13] ipalib: Load ipaserver plugins when api.env.in_server
 is True

https://fedorahosted.org/freeipa/ticket/3090
https://fedorahosted.org/freeipa/ticket/5073
---
 daemons/dnssec/ipa-dnskeysyncd                          | 1 -
 install/tools/ipa-replica-manage                        | 1 -
 ipalib/__init__.py                                      | 2 +-
 ipalib/plugins/baseuser.py                              | 2 --
 ipalib/plugins/stageuser.py                             | 2 --
 ipalib/plugins/user.py                                  | 2 +-
 ipapython/dnssec/ldapkeydb.py                           | 1 -
 ipaserver/dcerpc.py                                     | 1 -
 ipaserver/install/bindinstance.py                       | 1 -
 ipaserver/install/ipa_server_certinstall.py             | 2 +-
 ipaserver/install/ipa_server_upgrade.py                 | 1 -
 ipaserver/install/ldapupdate.py                         | 1 -
 ipaserver/install/plugins/update_managed_permissions.py | 1 -
 ipaserver/install/server/install.py                     | 3 ---
 ipaserver/rpcserver.py                                  | 6 ++++--
 makeaci                                                 | 1 +
 16 files changed, 8 insertions(+), 20 deletions(-)

diff --git a/daemons/dnssec/ipa-dnskeysyncd b/daemons/dnssec/ipa-dnskeysyncd
index 7213168..a0fcf8b 100755
--- a/daemons/dnssec/ipa-dnskeysyncd
+++ b/daemons/dnssec/ipa-dnskeysyncd
@@ -17,7 +17,6 @@ from ipapython.dn import DN
 from ipapython.ipa_log_manager import root_logger, standard_logging_setup
 from ipapython import ipaldap
 from ipapython import ipautil
-from ipaserver.plugins.ldap2 import ldap2
 from ipaplatform.paths import paths
 
 from ipapython.dnssec.keysyncer import KeySyncer
diff --git a/install/tools/ipa-replica-manage b/install/tools/ipa-replica-manage
index 57e30bc..281bfa7 100755
--- a/install/tools/ipa-replica-manage
+++ b/install/tools/ipa-replica-manage
@@ -31,7 +31,6 @@ from ipapython import ipautil
 from ipaserver.install import replication, dsinstance, installutils
 from ipaserver.install import bindinstance, cainstance, certs
 from ipaserver.install import opendnssecinstance, dnskeysyncinstance
-from ipaserver.plugins import ldap2
 from ipapython import version, ipaldap
 from ipalib import api, errors, util
 from ipalib.constants import CACERT
diff --git a/ipalib/__init__.py b/ipalib/__init__.py
index 0b58790..cc14925 100644
--- a/ipalib/__init__.py
+++ b/ipalib/__init__.py
@@ -905,7 +905,7 @@ class API(plugable.API):
     def bootstrap(self, parser=None, **overrides):
         super(API, self).bootstrap(parser, **overrides)
 
-        if self.env.context in ('server', 'lite'):
+        if self.env.in_server:
             self.packages.append('ipaserver')
         if self.env.context in ('installer', 'updates'):
             self.packages.append('ipaserver/install/plugins')
diff --git a/ipalib/plugins/baseuser.py b/ipalib/plugins/baseuser.py
index d2bc68f..9068ef0 100644
--- a/ipalib/plugins/baseuser.py
+++ b/ipalib/plugins/baseuser.py
@@ -38,8 +38,6 @@ from ipapython.ipavalidate import Email
 from ipalib.capabilities import client_has_capability
 from ipalib.util import (normalize_sshpubkey, validate_sshpubkey,
     convert_sshpubkey_post)
-if api.env.in_server and api.env.context in ['lite', 'server']:
-    from ipaserver.plugins.ldap2 import ldap2
 
 __doc__ = _("""
 Baseuser
diff --git a/ipalib/plugins/stageuser.py b/ipalib/plugins/stageuser.py
index 18e09e9..c840ad4 100644
--- a/ipalib/plugins/stageuser.py
+++ b/ipalib/plugins/stageuser.py
@@ -42,8 +42,6 @@ from ipapython.ipavalidate import Email
 from ipalib.capabilities import client_has_capability
 from ipalib.util import (normalize_sshpubkey, validate_sshpubkey,
     convert_sshpubkey_post)
-if api.env.in_server and api.env.context in ['lite', 'server']:
-    from ipaserver.plugins.ldap2 import ldap2
 
 __doc__ = _("""
 Stageusers
diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py
index d2404e2..8d1577b 100644
--- a/ipalib/plugins/user.py
+++ b/ipalib/plugins/user.py
@@ -43,7 +43,7 @@ from ipapython.ipavalidate import Email
 from ipalib.capabilities import client_has_capability
 from ipalib.util import (normalize_sshpubkey, validate_sshpubkey,
     convert_sshpubkey_post)
-if api.env.in_server and api.env.context in ['lite', 'server']:
+if api.env.in_server:
     from ipaserver.plugins.ldap2 import ldap2
 
 __doc__ = _("""
diff --git a/ipapython/dnssec/ldapkeydb.py b/ipapython/dnssec/ldapkeydb.py
index 520b510..23e6b01 100644
--- a/ipapython/dnssec/ldapkeydb.py
+++ b/ipapython/dnssec/ldapkeydb.py
@@ -11,7 +11,6 @@ import ipalib
 from ipapython.dn import DN
 from ipapython import ipaldap
 from ipapython import ipautil
-from ipaserver.plugins.ldap2 import ldap2
 from ipaplatform.paths import paths
 
 from abshsm import attrs_name2id, attrs_id2name, bool_attr_names, populate_pkcs11_metadata, AbstractHSM
diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py
index a1c57d2..ee92664 100644
--- a/ipaserver/dcerpc.py
+++ b/ipaserver/dcerpc.py
@@ -31,7 +31,6 @@ from ipapython import ipautil
 from ipapython.ipa_log_manager import *
 from ipapython.dn import DN
 from ipaserver.install import installutils
-from ipaserver.plugins import ldap2
 from ipalib.util import normalize_name
 
 import os, string, struct, copy
diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py
index 77ff342..102a8e5 100644
--- a/ipaserver/install/bindinstance.py
+++ b/ipaserver/install/bindinstance.py
@@ -29,7 +29,6 @@ import ldap
 
 import installutils
 import service
-from ipaserver.plugins import ldap2
 from ipaserver.install.cainstance import IPA_CA_RECORD
 from ipapython import sysrestore, ipautil, ipaldap
 from ipapython.ipa_log_manager import *
diff --git a/ipaserver/install/ipa_server_certinstall.py b/ipaserver/install/ipa_server_certinstall.py
index 9e24c4c..2e3e34a 100644
--- a/ipaserver/install/ipa_server_certinstall.py
+++ b/ipaserver/install/ipa_server_certinstall.py
@@ -31,7 +31,7 @@ from ipapython.ipautil import user_input, write_tmp_file
 from ipalib import api, errors
 from ipalib.constants import CACERT
 from ipaserver.install import certs, dsinstance, httpinstance, installutils
-from ipaserver.plugins.ldap2 import ldap2
+
 
 class ServerCertInstall(admintool.AdminTool):
     command_name = 'ipa-server-certinstall'
diff --git a/ipaserver/install/ipa_server_upgrade.py b/ipaserver/install/ipa_server_upgrade.py
index 8373b21..d0a839d 100644
--- a/ipaserver/install/ipa_server_upgrade.py
+++ b/ipaserver/install/ipa_server_upgrade.py
@@ -41,7 +41,6 @@ class ServerUpgrade(admintool.AdminTool):
         super(ServerUpgrade, self).run()
 
         api.bootstrap(in_server=True, context='updates')
-        import ipaserver.plugins.dogtag  # ensure profile backend gets loaded
         api.finalize()
 
         try:
diff --git a/ipaserver/install/ldapupdate.py b/ipaserver/install/ldapupdate.py
index f30659f..848ff1d 100644
--- a/ipaserver/install/ldapupdate.py
+++ b/ipaserver/install/ldapupdate.py
@@ -45,7 +45,6 @@ from ipaplatform import services
 from ipapython.dn import DN
 from ipapython.ipa_log_manager import *
 from ipapython.ipautil import wait_for_open_socket
-from ipaserver.plugins import ldap2
 
 UPDATES_DIR=paths.UPDATES_DIR
 
diff --git a/ipaserver/install/plugins/update_managed_permissions.py b/ipaserver/install/plugins/update_managed_permissions.py
index 11765fb..a2f289f 100644
--- a/ipaserver/install/plugins/update_managed_permissions.py
+++ b/ipaserver/install/plugins/update_managed_permissions.py
@@ -91,7 +91,6 @@ from ipalib.plugins.permission import permission, permission_del
 from ipalib.aci import ACI
 from ipalib import Updater
 from ipapython import ipautil
-from ipaserver.plugins.ldap2 import ldap2
 
 register = Registry()
 
diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py
index bde3485..2c5183b 100644
--- a/ipaserver/install/server/install.py
+++ b/ipaserver/install/server/install.py
@@ -592,9 +592,6 @@ def install_check(installer):
         api.env.ca_host = host_name
 
     api.bootstrap(**cfg)
-    if setup_ca:
-        # ensure profile backend is available
-        import ipaserver.plugins.dogtag
     api.finalize()
 
     if setup_ca:
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index 5158c42..43ee63e 100644
--- a/ipaserver/rpcserver.py
+++ b/ipaserver/rpcserver.py
@@ -748,7 +748,8 @@ class jsonserver_session(jsonserver, KerberosSession):
 
     def __init__(self):
         super(jsonserver_session, self).__init__()
-        auth_mgr = AuthManagerKerb(self.__class__.__name__)
+        name = '{0}_{1}'.format(self.__class__.__name__, id(self))
+        auth_mgr = AuthManagerKerb(name)
         session_mgr.auth_mgr.register(auth_mgr.name, auth_mgr)
 
     def _on_finalize(self):
@@ -1200,7 +1201,8 @@ class xmlserver_session(xmlserver, KerberosSession):
 
     def __init__(self):
         super(xmlserver_session, self).__init__()
-        auth_mgr = AuthManagerKerb(self.__class__.__name__)
+        name = '{0}_{1}'.format(self.__class__.__name__, id(self))
+        auth_mgr = AuthManagerKerb(name)
         session_mgr.auth_mgr.register(auth_mgr.name, auth_mgr)
 
     def _on_finalize(self):
diff --git a/makeaci b/makeaci
index ab65a99..5c441c0 100755
--- a/makeaci
+++ b/makeaci
@@ -95,6 +95,7 @@ def main(options):
         basedn=DN('dc=ipa,dc=example'),
         realm='IPA.EXAMPLE',
     )
+    from ipaserver.plugins import ldap2
     from ipaserver.install.plugins.update_managed_permissions import (
         update_managed_permissions)
     from ipalib.plugins.permission import permission
-- 
2.1.0

>From 13daaa1041c279b19a5f59314a9a8eb435a091ac Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 15 Jun 2015 10:53:22 +0000
Subject: [PATCH 03/13] ipalib: Move find_modules_in_dir from util to plugable

https://fedorahosted.org/freeipa/ticket/3090
---
 ipalib/plugable.py | 25 +++++++++++++++++++++++--
 ipalib/util.py     | 22 ----------------------
 2 files changed, 23 insertions(+), 24 deletions(-)

diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index ad662e5..74ac62b 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -38,7 +38,6 @@ import textwrap
 import collections
 
 from config import Env
-import util
 import text
 from text import _
 from base import ReadOnly, NameSpace, lock, islocked, check_name
@@ -61,6 +60,28 @@ def is_production_mode(obj):
     return obj.env.mode == 'production'
 
 
+# FIXME: This function has no unit test
+def find_modules_in_dir(src_dir):
+    """
+    Iterate through module names found in ``src_dir``.
+    """
+    if not (os.path.abspath(src_dir) == src_dir and os.path.isdir(src_dir)):
+        return
+    if os.path.islink(src_dir):
+        return
+    suffix = '.py'
+    for name in sorted(os.listdir(src_dir)):
+        if not name.endswith(suffix):
+            continue
+        pyfile = os.path.join(src_dir, name)
+        if not os.path.isfile(pyfile):
+            continue
+        module = name[:-len(suffix)]
+        if module == '__init__':
+            continue
+        yield (module, pyfile)
+
+
 class Registry(object):
     """A decorator that makes plugins available to the API
 
@@ -625,7 +646,7 @@ class API(DictProxy):
                 name=subpackage, file=plugins.__file__
             )
         self.log.debug('importing all plugin modules in %r...', plugins_dir)
-        for (name, pyfile) in util.find_modules_in_dir(plugins_dir):
+        for (name, pyfile) in find_modules_in_dir(plugins_dir):
             fullname = '%s.%s' % (subpackage, name)
             self.log.debug('importing plugin module %r', pyfile)
             try:
diff --git a/ipalib/util.py b/ipalib/util.py
index 44478a2..05b8561 100644
--- a/ipalib/util.py
+++ b/ipalib/util.py
@@ -75,28 +75,6 @@ def get_current_principal():
         raise errors.CCacheError()
 
 
-# FIXME: This function has no unit test
-def find_modules_in_dir(src_dir):
-    """
-    Iterate through module names found in ``src_dir``.
-    """
-    if not (os.path.abspath(src_dir) == src_dir and os.path.isdir(src_dir)):
-        return
-    if os.path.islink(src_dir):
-        return
-    suffix = '.py'
-    for name in sorted(os.listdir(src_dir)):
-        if not name.endswith(suffix):
-            continue
-        pyfile = os.path.join(src_dir, name)
-        if not os.path.isfile(pyfile):
-            continue
-        module = name[:-len(suffix)]
-        if module == '__init__':
-            continue
-        yield (module, pyfile)
-
-
 def validate_host_dns(log, fqdn):
     """
     See if the hostname has a DNS A/AAAA record.
-- 
2.1.0

>From 078729ad7d13fa303cd291ef7269acd162824c05 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 15 Jun 2015 11:02:07 +0000
Subject: [PATCH 04/13] plugable: Specify plugins to import in API by module
 names

This change removes the automatic plugins sub-package magic and allows
specifying modules in addition to packages.

https://fedorahosted.org/freeipa/ticket/3090
---
 ipalib/__init__.py       |  6 ++--
 ipalib/plugable.py       | 84 ++++++++++++++++++++++++------------------------
 ipaserver/advise/base.py |  2 +-
 3 files changed, 46 insertions(+), 46 deletions(-)

diff --git a/ipalib/__init__.py b/ipalib/__init__.py
index cc14925..74a8120 100644
--- a/ipalib/__init__.py
+++ b/ipalib/__init__.py
@@ -900,15 +900,15 @@ else:
 
 class API(plugable.API):
     def __init__(self, allowed):
-        super(API, self).__init__(allowed, ['ipalib'])
+        super(API, self).__init__(allowed, ['ipalib.plugins.*'])
 
     def bootstrap(self, parser=None, **overrides):
         super(API, self).bootstrap(parser, **overrides)
 
         if self.env.in_server:
-            self.packages.append('ipaserver')
+            self.modules.append('ipaserver.plugins.*')
         if self.env.context in ('installer', 'updates'):
-            self.packages.append('ipaserver/install/plugins')
+            self.modules.append('ipaserver.install.plugins.*')
 
 
 def create_api(mode='dummy'):
diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index 74ac62b..d98ab73 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -36,6 +36,7 @@ import optparse
 import errors
 import textwrap
 import collections
+import importlib
 
 from config import Env
 import text
@@ -79,7 +80,7 @@ def find_modules_in_dir(src_dir):
         module = name[:-len(suffix)]
         if module == '__init__':
             continue
-        yield (module, pyfile)
+        yield module
 
 
 class Registry(object):
@@ -429,9 +430,9 @@ class API(DictProxy):
 
     register = Registrar()
 
-    def __init__(self, allowed, packages):
+    def __init__(self, allowed, modules):
         self.__plugins = {base: {} for base in allowed}
-        self.packages = packages
+        self.modules = modules
         self.__d = dict()
         self.__done = set()
         self.env = Env()
@@ -609,56 +610,55 @@ class API(DictProxy):
         self.__do_if_not_done('bootstrap')
         if self.env.mode in ('dummy', 'unit_test'):
             return
-        for package in self.packages:
-            self.import_plugins(package)
+        for module in self.modules:
+            self.import_plugins(module)
         for klass, kwargs in self.register.iteritems():
             self.add_plugin(klass, **kwargs)
 
     # FIXME: This method has no unit test
-    def import_plugins(self, package):
+    def import_plugins(self, module):
         """
-        Import modules in ``plugins`` sub-package of ``package``.
+        Import plugins from ``module``.
+
+        :param module: Name of the module to import. This might be a wildcard
+                       in the form ```package.*``` in which case all modules
+                       from the given package are loaded.
         """
-        package = package.replace(os.path.sep, '.')
-        subpackage = '%s.plugins' % package
-        try:
-            parent = __import__(package)
-            parts = package.split('.')[1:]
-            if parts:
-                for part in parts:
-                    if part == 'plugins':
-                        plugins = subpackage.plugins
-                        subpackage = plugins.__name__
-                        break
-                    subpackage = parent.__getattribute__(part)
-                    parent = subpackage
-            else:
-                plugins = __import__(subpackage).plugins
-        except ImportError, e:
-            self.log.error(
-                'cannot import plugins sub-package %s: %s', subpackage, e
-            )
-            raise e
-        parent_dir = path.dirname(path.abspath(parent.__file__))
-        plugins_dir = path.dirname(path.abspath(plugins.__file__))
-        if parent_dir == plugins_dir:
-            raise errors.PluginsPackageError(
-                name=subpackage, file=plugins.__file__
-            )
-        self.log.debug('importing all plugin modules in %r...', plugins_dir)
-        for (name, pyfile) in find_modules_in_dir(plugins_dir):
-            fullname = '%s.%s' % (subpackage, name)
-            self.log.debug('importing plugin module %r', pyfile)
+        if module.endswith('.*'):
+            subpackage = module[:-2]
             try:
-                __import__(fullname)
-            except errors.SkipPluginModule, e:
-                self.log.debug(
-                    'skipping plugin module %s: %s', fullname, e.reason
+                plugins = importlib.import_module(subpackage)
+            except ImportError, e:
+                self.log.error("cannot import plugins sub-package %s: %s",
+                               subpackage, e)
+                raise
+            package, dot, part = subpackage.rpartition('.')
+            parent = sys.modules[package]
+
+            parent_dir = path.dirname(path.abspath(parent.__file__))
+            plugins_dir = path.dirname(path.abspath(plugins.__file__))
+            if parent_dir == plugins_dir:
+                raise errors.PluginsPackageError(
+                    name=subpackage, file=plugins.__file__
                 )
+
+            self.log.debug("importing all plugin modules in %s...", subpackage)
+            modules = find_modules_in_dir(plugins_dir)
+            modules = ['.'.join((subpackage, name)) for name in modules]
+        else:
+            modules = [module]
+
+        for name in modules:
+            self.log.debug("importing plugin module %s", name)
+            try:
+                importlib.import_module(name)
+            except errors.SkipPluginModule, e:
+                self.log.debug("skipping plugin module %s: %s", name, e.reason)
             except StandardError, e:
                 if self.env.startup_traceback:
                     import traceback
-                    self.log.error('could not load plugin module %r\n%s', pyfile, traceback.format_exc())
+                    self.log.error("could not load plugin module %s\n%s", name,
+                                   traceback.format_exc())
                 raise
 
     def add_plugin(self, klass, override=False):
diff --git a/ipaserver/advise/base.py b/ipaserver/advise/base.py
index ab8323c..9913e55 100644
--- a/ipaserver/advise/base.py
+++ b/ipaserver/advise/base.py
@@ -121,7 +121,7 @@ class Advice(Plugin):
 
         raise NotImplementedError
 
-advise_api = API((Advice,), ('ipaserver/advise/plugins',))
+advise_api = API((Advice,), ('ipaserver.advise.plugins.*',))
 
 
 class IpaAdvise(admintool.AdminTool):
-- 
2.1.0

>From 46dd843f0be6db66ed2c174becf431ac5358ad91 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 22 Jun 2015 10:16:34 +0000
Subject: [PATCH 05/13] plugable: Load plugins only from modules imported by
 API

Previously all plugin modules imported from anywhere were added to the API.

https://fedorahosted.org/freeipa/ticket/3090
---
 ipalib/__init__.py                    |  4 ++--
 ipalib/cli.py                         |  3 +--
 ipalib/plugable.py                    | 24 +++++++++++++++++++++---
 ipatests/test_ipalib/test_backend.py  |  8 ++++----
 ipatests/test_ipalib/test_crud.py     |  4 ++--
 ipatests/test_ipalib/test_frontend.py | 12 ++++++------
 ipatests/test_ipalib/test_plugable.py |  2 +-
 ipatests/test_ipaserver/test_ldap.py  |  8 ++++----
 ipatests/util.py                      |  4 ++--
 makeaci                               |  6 ++----
 10 files changed, 45 insertions(+), 30 deletions(-)

diff --git a/ipalib/__init__.py b/ipalib/__init__.py
index 74a8120..a6fad54 100644
--- a/ipalib/__init__.py
+++ b/ipalib/__init__.py
@@ -936,7 +936,7 @@ api = create_api(mode=None)
 
 if os.environ.get('IPA_UNIT_TEST_MODE', None) == 'cli_test':
     from cli import cli_plugins
-    for klass in cli_plugins:
-        api.register(klass)
     api.bootstrap(context='cli', in_server=False, in_tree=True)
+    for klass in cli_plugins:
+        api.add_plugin(klass)
     api.finalize()
diff --git a/ipalib/cli.py b/ipalib/cli.py
index 52529ea..c0e8c9c 100644
--- a/ipalib/cli.py
+++ b/ipalib/cli.py
@@ -1333,8 +1333,7 @@ def run(api):
     try:
         (options, argv) = api.bootstrap_with_global_options(context='cli')
         for klass in cli_plugins:
-            api.register(klass)
-        api.load_plugins()
+            api.add_plugin(klass)
         api.finalize()
         if not 'config_loaded' in api.env and not 'help' in argv:
             raise NotConfiguredError()
diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index d98ab73..d13e5ed 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -612,8 +612,6 @@ class API(DictProxy):
             return
         for module in self.modules:
             self.import_plugins(module)
-        for klass, kwargs in self.register.iteritems():
-            self.add_plugin(klass, **kwargs)
 
     # FIXME: This method has no unit test
     def import_plugins(self, module):
@@ -651,7 +649,7 @@ class API(DictProxy):
         for name in modules:
             self.log.debug("importing plugin module %s", name)
             try:
-                importlib.import_module(name)
+                module = importlib.import_module(name)
             except errors.SkipPluginModule, e:
                 self.log.debug("skipping plugin module %s: %s", name, e.reason)
             except StandardError, e:
@@ -660,6 +658,23 @@ class API(DictProxy):
                     self.log.error("could not load plugin module %s\n%s", name,
                                    traceback.format_exc())
                 raise
+            else:
+                self.add_module(module)
+
+    def add_module(self, module):
+        """
+        Add plugins from the ``module``.
+
+        :param module: A module from which to add plugins.
+        """
+        for name in dir(module):
+            klass = getattr(module, name)
+            if not inspect.isclass(klass):
+                continue
+            if klass not in self.register:
+                continue
+            kwargs = self.register[klass]
+            self.add_plugin(klass, **kwargs)
 
     def add_plugin(self, klass, override=False):
         """
@@ -679,6 +694,9 @@ class API(DictProxy):
 
             found = True
 
+            if sub_d.get(klass.__name__) is klass:
+                continue
+
             # Check override:
             if klass.__name__ in sub_d:
                 if not override:
diff --git a/ipatests/test_ipalib/test_backend.py b/ipatests/test_ipalib/test_backend.py
index 121c474..91eb920 100644
--- a/ipatests/test_ipalib/test_backend.py
+++ b/ipatests/test_ipalib/test_backend.py
@@ -186,7 +186,7 @@ class test_Executioner(ClassChecker):
             def execute(self, *args, **options):
                 assert type(args[1]) is tuple
                 return dict(result=args + (options,))
-        api.register(echo)
+        api.add_plugin(echo)
 
         class good(Command):
             def execute(self, **options):
@@ -194,12 +194,12 @@ class test_Executioner(ClassChecker):
                     name='nurse',
                     error=u'Not naughty!',
                 )
-        api.register(good)
+        api.add_plugin(good)
 
         class bad(Command):
             def execute(self, **options):
                 raise ValueError('This is private.')
-        api.register(bad)
+        api.add_plugin(bad)
 
         class with_name(Command):
             """
@@ -208,7 +208,7 @@ class test_Executioner(ClassChecker):
             takes_options = 'name'
             def execute(self, **options):
                 return dict(result=options['name'].upper())
-        api.register(with_name)
+        api.add_plugin(with_name)
 
         api.finalize()
         o = self.cls()
diff --git a/ipatests/test_ipalib/test_crud.py b/ipatests/test_ipalib/test_crud.py
index 6d29ab9..f173716 100644
--- a/ipatests/test_ipalib/test_crud.py
+++ b/ipatests/test_ipalib/test_crud.py
@@ -47,8 +47,8 @@ class CrudChecker(ClassChecker):
         class user_verb(self.cls):
             takes_args = args
             takes_options = options
-        api.register(user)
-        api.register(user_verb)
+        api.add_plugin(user)
+        api.add_plugin(user_verb)
         api.finalize()
         return api
 
diff --git a/ipatests/test_ipalib/test_frontend.py b/ipatests/test_ipalib/test_frontend.py
index b4fbf3e..c47113f 100644
--- a/ipatests/test_ipalib/test_frontend.py
+++ b/ipatests/test_ipalib/test_frontend.py
@@ -817,7 +817,7 @@ class test_LocalOrRemote(ClassChecker):
 
         # Test when in_server=False:
         (api, home) = create_test_api(in_server=False)
-        api.register(example)
+        api.add_plugin(example)
         api.finalize()
         cmd = api.Command.example
         assert cmd(version=u'2.47') == dict(
@@ -835,7 +835,7 @@ class test_LocalOrRemote(ClassChecker):
 
         # Test when in_server=True (should always call execute):
         (api, home) = create_test_api(in_server=True)
-        api.register(example)
+        api.add_plugin(example)
         api.finalize()
         cmd = api.Command.example
         assert cmd(version=u'2.47') == dict(
@@ -1012,10 +1012,10 @@ class test_Object(ClassChecker):
         (api, home) = create_test_api()
         class ldap(backend.Backend):
             whatever = 'It worked!'
-        api.register(ldap)
+        api.add_plugin(ldap)
         class user(frontend.Object):
             backend_name = 'ldap'
-        api.register(user)
+        api.add_plugin(user)
         api.finalize()
         b = api.Object.user.backend
         assert isinstance(b, ldap)
@@ -1118,8 +1118,8 @@ class test_Method(ClassChecker):
         class user_verb(self.cls):
             takes_args = args
             takes_options = options
-        api.register(user)
-        api.register(user_verb)
+        api.add_plugin(user)
+        api.add_plugin(user_verb)
         api.finalize()
         return api
 
diff --git a/ipatests/test_ipalib/test_plugable.py b/ipatests/test_ipalib/test_plugable.py
index 2a6f8aa..b0f6070 100644
--- a/ipatests/test_ipalib/test_plugable.py
+++ b/ipatests/test_ipalib/test_plugable.py
@@ -384,7 +384,7 @@ class test_API(ClassChecker):
         api = plugable.API([base0, base1], [])
         api.env.mode = 'unit_test'
         api.env.in_tree = True
-        r = api.register
+        r = api.add_plugin
 
         class base0_plugin0(base0):
             pass
diff --git a/ipatests/test_ipaserver/test_ldap.py b/ipatests/test_ipaserver/test_ldap.py
index 8b4e500..2c187b0 100644
--- a/ipatests/test_ipaserver/test_ldap.py
+++ b/ipatests/test_ipaserver/test_ldap.py
@@ -110,10 +110,10 @@ class test_ldap(object):
         # we need for the test.
         myapi = create_api(mode=None)
         myapi.bootstrap(context='cli', in_server=True, in_tree=True)
-        myapi.register(ldap2)
-        myapi.register(host)
-        myapi.register(service)
-        myapi.register(service_show)
+        myapi.add_plugin(ldap2)
+        myapi.add_plugin(host)
+        myapi.add_plugin(service)
+        myapi.add_plugin(service_show)
         myapi.finalize()
 
         pwfile = api.env.dot_ipa + os.sep + ".dmpw"
diff --git a/ipatests/util.py b/ipatests/util.py
index 247714a..a8899cc 100644
--- a/ipatests/util.py
+++ b/ipatests/util.py
@@ -505,9 +505,9 @@ class PluginTester(object):
         :param kw: Additional \**kw args to pass to `create_test_api`.
         """
         (api, home) = create_test_api(**kw)
-        api.register(self.plugin)
+        api.add_plugin(self.plugin)
         for p in plugins:
-            api.register(p)
+            api.add_plugin(p)
         return (api, home)
 
     def finalize(self, *plugins, **kw):
diff --git a/makeaci b/makeaci
index 5c441c0..e850609 100755
--- a/makeaci
+++ b/makeaci
@@ -95,10 +95,8 @@ def main(options):
         basedn=DN('dc=ipa,dc=example'),
         realm='IPA.EXAMPLE',
     )
-    from ipaserver.plugins import ldap2
-    from ipaserver.install.plugins.update_managed_permissions import (
-        update_managed_permissions)
-    from ipalib.plugins.permission import permission
+    api.import_plugins('ipaserver.plugins.ldap2')
+    api.import_plugins('ipaserver.install.plugins.update_managed_permissions')
     api.finalize()
 
     output_lines = list(generate_aci_lines(api))
-- 
2.1.0

>From 55b1eff76dd5bfd720f8fbef77894fb6fd342b47 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 22 Jun 2015 10:58:43 +0000
Subject: [PATCH 06/13] plugable: Pass API to plugins on initialization rather
 than using set_api

https://fedorahosted.org/freeipa/ticket/3090
---
 .../certmonger/dogtag-ipa-ca-renew-agent-submit    |   2 +-
 install/restart_scripts/renew_ca_cert              |   2 +-
 install/tools/ipa-compat-manage                    |   2 +-
 install/tools/ipa-nis-manage                       |   2 +-
 ipalib/__init__.py                                 |   4 +-
 ipalib/backend.py                                  |   4 +-
 ipalib/cli.py                                      |   2 +-
 ipalib/frontend.py                                 |   7 +-
 ipalib/plugable.py                                 |  50 +++----
 ipalib/plugins/migration.py                        |   2 +-
 ipalib/plugins/misc.py                             |   7 +-
 ipalib/plugins/user.py                             |   4 +-
 ipaserver/advise/base.py                           |   4 +-
 ipaserver/install/bindinstance.py                  |   2 +-
 ipaserver/install/cainstance.py                    |   6 +-
 ipaserver/install/ipa_cacert_manage.py             |   2 +-
 ipaserver/install/ipa_otptoken_import.py           |   2 +-
 ipaserver/install/ipa_replica_prepare.py           |   2 +-
 ipaserver/install/server/install.py                |   3 +-
 ipaserver/plugins/dogtag.py                        |  12 +-
 ipaserver/plugins/ldap2.py                         |  45 +-----
 ipaserver/plugins/rabase.py                        |   4 +-
 ipaserver/rpcserver.py                             |  36 ++---
 ipatests/test_ipalib/test_backend.py               |  21 ++-
 ipatests/test_ipalib/test_cli.py                   |   3 +-
 ipatests/test_ipalib/test_crud.py                  |   5 +-
 ipatests/test_ipalib/test_frontend.py              | 166 ++++++++++-----------
 ipatests/test_ipalib/test_output.py                |   3 +-
 ipatests/test_ipalib/test_plugable.py              |  26 +---
 ipatests/test_ipaserver/test_ldap.py               |  10 +-
 ipatests/test_ipaserver/test_rpcserver.py          |  18 ++-
 ipatests/test_xmlrpc/test_baseldap_plugin.py       |  15 +-
 ipatests/test_xmlrpc/test_dns_plugin.py            |   2 +-
 ipatests/test_xmlrpc/test_permission_plugin.py     |   2 +-
 ipatests/test_xmlrpc/testcert.py                   |   2 +-
 35 files changed, 205 insertions(+), 274 deletions(-)

diff --git a/install/certmonger/dogtag-ipa-ca-renew-agent-submit b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
index 66f3bf7..e833a31 100755
--- a/install/certmonger/dogtag-ipa-ca-renew-agent-submit
+++ b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
@@ -58,7 +58,7 @@ OPERATION_NOT_SUPPORTED_BY_HELPER = 6
 def ldap_connect():
     conn = None
     try:
-        conn = ldap2(shared_instance=False, ldap_uri=api.env.ldap_uri)
+        conn = ldap2(api)
         conn.connect(ccache=os.environ['KRB5CCNAME'])
         yield conn
     finally:
diff --git a/install/restart_scripts/renew_ca_cert b/install/restart_scripts/renew_ca_cert
index 95205e4..86f5765 100644
--- a/install/restart_scripts/renew_ca_cert
+++ b/install/restart_scripts/renew_ca_cert
@@ -140,7 +140,7 @@ def _main():
 
             conn = None
             try:
-                conn = ldap2(shared_instance=False, ldap_uri=api.env.ldap_uri)
+                conn = ldap2(api)
                 conn.connect(ccache=ccache_filename)
             except Exception, e:
                 syslog.syslog(
diff --git a/install/tools/ipa-compat-manage b/install/tools/ipa-compat-manage
index 23b528f..7d9d20c 100755
--- a/install/tools/ipa-compat-manage
+++ b/install/tools/ipa-compat-manage
@@ -107,7 +107,7 @@ def main():
     conn = None
     try:
         try:
-            conn = ldap2(shared_instance=False, base_dn='')
+            conn = ldap2(api)
             conn.connect(
                 bind_dn=DN(('cn', 'directory manager')), bind_pw=dirman_password
             )
diff --git a/install/tools/ipa-nis-manage b/install/tools/ipa-nis-manage
index b412bb0..51cd6c3 100755
--- a/install/tools/ipa-nis-manage
+++ b/install/tools/ipa-nis-manage
@@ -120,7 +120,7 @@ def main():
     conn = None
     try:
         try:
-            conn = ldap2(shared_instance=False, base_dn='')
+            conn = ldap2(api)
             conn.connect(
                 bind_dn=DN(('cn', 'directory manager')), bind_pw=dirman_password
             )
diff --git a/ipalib/__init__.py b/ipalib/__init__.py
index a6fad54..4a66e7d 100644
--- a/ipalib/__init__.py
+++ b/ipalib/__init__.py
@@ -292,7 +292,7 @@ does that, and the backend plugins are only installed on the server.  The
 ``user_add.execute()`` method, which is only called when in a server context,
 is implemented as a series of calls to methods on the ``ldap`` backend plugin.
 
-When `plugable.Plugin.set_api()` is called, each plugin stores a reference to
+When `plugable.Plugin.__init__()` is called, each plugin stores a reference to
 the `plugable.API` instance it has been loaded into.  So your plugin can
 access the ``my_backend`` plugin as ``self.api.Backend.my_backend``.
 
@@ -668,7 +668,7 @@ See the `ipalib.cli.textui` plugin for a description of its methods.
 Logging from your plugin
 ------------------------
 
-After `plugable.Plugin.set_api()` is called, your plugin will have a
+After `plugable.Plugin.__init__()` is called, your plugin will have a
 ``self.log`` attribute.  Plugins should only log through this attribute.
 For example:
 
diff --git a/ipalib/backend.py b/ipalib/backend.py
index 0f381cb..d510bc7 100644
--- a/ipalib/backend.py
+++ b/ipalib/backend.py
@@ -43,8 +43,8 @@ class Connectible(Backend):
     `request.destroy_context()` can properly close all open connections.
     """
 
-    def __init__(self, shared_instance=False):
-        Backend.__init__(self)
+    def __init__(self, api, shared_instance=False):
+        Backend.__init__(self, api)
         if shared_instance:
             self.id = self.name
         else:
diff --git a/ipalib/cli.py b/ipalib/cli.py
index c0e8c9c..8515b91 100644
--- a/ipalib/cli.py
+++ b/ipalib/cli.py
@@ -715,7 +715,7 @@ class help(frontend.Local):
         self._builtins = []
 
         # build help topics
-        for c in self.Command():
+        for c in self.api.Command():
             if c.NO_CLI:
                 continue
 
diff --git a/ipalib/frontend.py b/ipalib/frontend.py
index 0b42cb6..c36bfca 100644
--- a/ipalib/frontend.py
+++ b/ipalib/frontend.py
@@ -1250,12 +1250,12 @@ class Attribute(Plugin):
     # Create stubs for attributes that are set in _on_finalize()
     __obj = Plugin.finalize_attr('_Attribute__obj')
 
-    def __init__(self):
+    def __init__(self, api):
         m = self.NAME_REGEX.match(type(self).__name__)
         assert m
         self.__obj_name = m.group('obj')
         self.__attr_name = m.group('attr')
-        super(Attribute, self).__init__()
+        super(Attribute, self).__init__(api)
 
     def __get_obj_name(self):
         return self.__obj_name
@@ -1347,9 +1347,6 @@ class Method(Attribute, Command):
     extra_options_first = False
     extra_args_first = False
 
-    def __init__(self):
-        super(Method, self).__init__()
-
     def get_output_params(self):
         for param in self.obj.params():
             if 'no_output' in param.flags:
diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index d13e5ed..bdd19c2 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -224,8 +224,9 @@ class Plugin(ReadOnly):
 
     label = None
 
-    def __init__(self):
-        self.__api = None
+    def __init__(self, api):
+        assert api is not None
+        self.__api = api
         self.__finalize_called = False
         self.__finalized = False
         self.__finalize_lock = threading.RLock()
@@ -254,14 +255,27 @@ class Plugin(ReadOnly):
                 )
             )
 
-    def __get_api(self):
+    @property
+    def api(self):
         """
-        Return `API` instance passed to `set_api()`.
-
-        If `set_api()` has not yet been called, None is returned.
+        Return `API` instance passed to `__init__()`.
         """
         return self.__api
-    api = property(__get_api)
+
+    # FIXME: for backward compatibility only
+    @property
+    def env(self):
+        return self.__api.env
+
+    # FIXME: for backward compatibility only
+    @property
+    def Backend(self):
+        return self.__api.Backend
+
+    # FIXME: for backward compatibility only
+    @property
+    def Command(self):
+        return self.__api.Command
 
     def finalize(self):
         """
@@ -336,23 +350,6 @@ class Plugin(ReadOnly):
                     "attribute '%s' of plugin '%s' was not set in finalize()" % (self.name, obj.name)
                 )
 
-    def set_api(self, api):
-        """
-        Set reference to `API` instance.
-        """
-        assert self.__api is None, 'set_api() can only be called once'
-        assert api is not None, 'set_api() argument cannot be None'
-        self.__api = api
-        if not isinstance(api, API):
-            return
-        for name in api:
-            assert not hasattr(self, name)
-            setattr(self, name, api[name])
-        for name in ('env', 'context'):
-            if hasattr(api, name):
-                assert not hasattr(self, name)
-                setattr(self, name, getattr(api, name))
-
     def call(self, executable, *args):
         """
         Call ``executable`` with ``args`` using subprocess.call().
@@ -746,7 +743,7 @@ class API(DictProxy):
                 try:
                     instance = plugins[klass]
                 except KeyError:
-                    instance = plugins[klass] = klass()
+                    instance = plugins[klass] = klass(self)
                 members.append(instance)
                 plugin_info.setdefault(
                     '%s.%s' % (klass.__module__, klass.__name__),
@@ -760,9 +757,6 @@ class API(DictProxy):
             self.__d[name] = namespace
             object.__setattr__(self, name, namespace)
 
-        for instance in plugins.itervalues():
-            instance.set_api(self)
-
         for klass, instance in plugins.iteritems():
             if not production_mode:
                 assert instance.api is self
diff --git a/ipalib/plugins/migration.py b/ipalib/plugins/migration.py
index 9dced13..6aeb942 100644
--- a/ipalib/plugins/migration.py
+++ b/ipalib/plugins/migration.py
@@ -879,7 +879,7 @@ can use their Kerberos accounts.''')
             return dict(result={}, failed={}, enabled=False, compat=True)
 
         # connect to DS
-        ds_ldap = ldap2(shared_instance=False, ldap_uri=ldapuri, base_dn='')
+        ds_ldap = ldap2(self.api, ldap_uri=ldapuri)
 
         cacert = None
         if options.get('cacertfile') is not None:
diff --git a/ipalib/plugins/misc.py b/ipalib/plugins/misc.py
index 2c932ca..67bb929 100644
--- a/ipalib/plugins/misc.py
+++ b/ipalib/plugins/misc.py
@@ -133,11 +133,6 @@ class plugins(LocalOrRemote):
     )
 
     def execute(self, **options):
-        plugins = sorted(self.api.plugins, key=lambda o: o.plugin)
         return dict(
-            result=dict(
-                (p.plugin, p.bases) for p in plugins
-            ),
-            count=len(plugins),
+            result=dict(self.api.plugins),
         )
-
diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py
index 8d1577b..da6e4eb 100644
--- a/ipalib/plugins/user.py
+++ b/ipalib/plugins/user.py
@@ -940,9 +940,7 @@ class user_status(LDAPQuery):
             if host == api.env.host:
                 other_ldap = self.obj.backend
             else:
-                other_ldap = ldap2(shared_instance=False,
-                                   ldap_uri='ldap://%s' % host,
-                                   base_dn=self.api.env.basedn)
+                other_ldap = ldap2(self.api, ldap_uri='ldap://%s' % host)
                 try:
                     other_ldap.connect(ccache=os.environ['KRB5CCNAME'])
                 except Exception, e:
diff --git a/ipaserver/advise/base.py b/ipaserver/advise/base.py
index 9913e55..e9873ac 100644
--- a/ipaserver/advise/base.py
+++ b/ipaserver/advise/base.py
@@ -104,8 +104,8 @@ class Advice(Plugin):
     require_root = False
     description = ''
 
-    def __init__(self):
-        super(Advice, self).__init__()
+    def __init__(self, api):
+        super(Advice, self).__init__(api)
         self.log = _AdviceOutput()
 
     def set_options(self, options):
diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py
index 102a8e5..2228342 100644
--- a/ipaserver/install/bindinstance.py
+++ b/ipaserver/install/bindinstance.py
@@ -1179,7 +1179,7 @@ class BindInstance(service.Service):
         print "Global DNS configuration in LDAP server is not empty"
         print "The following configuration options override local settings in named.conf:"
         print ""
-        textui = ipalib.cli.textui()
+        textui = ipalib.cli.textui(api)
         api.Command.dnsconfig_show.output_for_cli(textui, result, None, reverse=False)
 
     def uninstall(self):
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 563a198..a505be8 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -1588,7 +1588,7 @@ def update_people_entry(dercert):
     while attempts < 10:
         conn = None
         try:
-            conn = ldap2.ldap2(shared_instance=False, ldap_uri=dogtag_uri)
+            conn = ldap2.ldap2(api, ldap_uri=dogtag_uri)
             conn.connect(autobind=True)
 
             db_filter = conn.make_filter(
@@ -1652,7 +1652,7 @@ def configure_profiles_acl():
     )
     modlist = [(ldap.MOD_ADD, 'resourceACLS', [rule])]
 
-    conn = ldap2.ldap2(shared_instance=False, ldap_uri=dogtag_uri)
+    conn = ldap2.ldap2(api, ldap_uri=dogtag_uri)
     if not conn.isconnected():
         conn.connect(autobind=True)
     rules = conn.get_entry(dn).get('resourceACLS', [])
@@ -1673,7 +1673,7 @@ def import_included_profiles():
 
     server_id = installutils.realm_to_serverid(api.env.realm)
     dogtag_uri = 'ldapi://%%2fvar%%2frun%%2fslapd-%s.socket' % server_id
-    conn = ldap2.ldap2(shared_instance=False, ldap_uri=dogtag_uri)
+    conn = ldap2.ldap2(api, ldap_uri=dogtag_uri)
     if not conn.isconnected():
         conn.connect(autobind=True)
 
diff --git a/ipaserver/install/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py
index e074601..01ec805 100644
--- a/ipaserver/install/ipa_cacert_manage.py
+++ b/ipaserver/install/ipa_cacert_manage.py
@@ -121,7 +121,7 @@ class CACertManage(admintool.AdminTool):
         return rc
 
     def ldap_connect(self):
-        conn = ldap2()
+        conn = ldap2(api)
 
         password = self.options.password
         if not password:
diff --git a/ipaserver/install/ipa_otptoken_import.py b/ipaserver/install/ipa_otptoken_import.py
index c6a69c9..386ca42 100644
--- a/ipaserver/install/ipa_otptoken_import.py
+++ b/ipaserver/install/ipa_otptoken_import.py
@@ -507,7 +507,7 @@ class OTPTokenImport(admintool.AdminTool):
         api.bootstrap(in_server=True)
         api.finalize()
 
-        conn = ldap2()
+        conn = ldap2(api)
         try:
             ccache = krbV.default_context().default_ccache()
             conn.connect(ccache=ccache)
diff --git a/ipaserver/install/ipa_replica_prepare.py b/ipaserver/install/ipa_replica_prepare.py
index 3a2975b..c2e7a0f 100644
--- a/ipaserver/install/ipa_replica_prepare.py
+++ b/ipaserver/install/ipa_replica_prepare.py
@@ -637,7 +637,7 @@ class ReplicaPrepare(admintool.AdminTool):
             os.remove(agent_name)
 
     def update_pki_admin_password(self):
-        ldap = ldap2(shared_instance=False)
+        ldap = ldap2(api)
         ldap.connect(
             bind_dn=DN(('cn', 'directory manager')),
             bind_pw=self.dirman_password
diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py
index 2c5183b..7237635 100644
--- a/ipaserver/install/server/install.py
+++ b/ipaserver/install/server/install.py
@@ -240,8 +240,7 @@ def set_subject_in_config(realm_name, dm_password, suffix, subject_base):
             installutils.realm_to_serverid(realm_name)
         )
         try:
-            conn = ldap2(shared_instance=False, ldap_uri=ldapuri,
-                         base_dn=suffix)
+            conn = ldap2(api, ldap_uri=ldapuri)
             conn.connect(bind_dn=DN(('cn', 'directory manager')),
                          bind_pw=dm_password)
         except errors.ExecutionError, e:
diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index a5b7d23..3dc8f5c 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -1286,7 +1286,7 @@ class ra(rabase.rabase):
     """
     DEFAULT_PROFILE = dogtag.DEFAULT_PROFILE
 
-    def __init__(self):
+    def __init__(self, api):
         if api.env.in_tree:
             self.sec_dir = api.env.dot_ipa + os.sep + 'alias'
             self.pwd_file = self.sec_dir + os.sep + '.pwd'
@@ -1303,7 +1303,7 @@ class ra(rabase.rabase):
             f.close()
         except IOError:
             self.password = ''
-        super(ra, self).__init__()
+        super(ra, self).__init__(api)
 
     def raise_certificate_operation_error(self, func_name, err_msg=None, detail=None):
         """
@@ -1896,11 +1896,11 @@ class kra(Backend):
     KRA backend plugin (for Vault)
     """
 
-    def __init__(self, kra_port=443):
+    def __init__(self, api, kra_port=443):
 
         self.kra_port = kra_port
 
-        super(kra, self).__init__()
+        super(kra, self).__init__(api)
 
     def get_client(self):
         """
@@ -1958,7 +1958,7 @@ class RestClient(Backend):
         except:
             return None
 
-    def __init__(self):
+    def __init__(self, api):
         if api.env.in_tree:
             self.sec_dir = api.env.dot_ipa + os.sep + 'alias'
             self.pwd_file = self.sec_dir + os.sep + '.pwd'
@@ -1970,7 +1970,7 @@ class RestClient(Backend):
         self.ipa_certificate_nickname = "ipaCert"
         self.ca_certificate_nickname = "caCert"
         self._read_password()
-        super(RestClient, self).__init__()
+        super(RestClient, self).__init__(api)
 
         # session cookie
         self.override_port = None
diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py
index 36a6fed..68feee4 100644
--- a/ipaserver/plugins/ldap2.py
+++ b/ipaserver/plugins/ldap2.py
@@ -56,47 +56,20 @@ from ipalib.crud import CrudBackend
 from ipalib.request import context
 
 
-class ldap2(LDAPClient, CrudBackend):
+class ldap2(CrudBackend, LDAPClient):
     """
     LDAP Backend Take 2.
     """
 
-    def __init__(self, shared_instance=False, ldap_uri=None, base_dn=None,
-                 schema=None):
-        self.__ldap_uri = None
+    def __init__(self, api, ldap_uri=None):
+        if ldap_uri is None:
+            ldap_uri = api.env.ldap_uri
 
-        CrudBackend.__init__(self, shared_instance=shared_instance)
-        LDAPClient.__init__(self, ldap_uri)
+        force_schema_updates = api.env.context in ('installer', 'updates')
 
-        self.__base_dn = base_dn
-
-    @property
-    def api(self):
-        self_api = super(ldap2, self).api
-        if self_api is None:
-            self_api = api
-        return self_api
-
-    @property
-    def ldap_uri(self):
-        try:
-            return self.__ldap_uri or self.api.env.ldap_uri
-        except AttributeError:
-            return 'ldap://example.com'
-
-    @ldap_uri.setter
-    def ldap_uri(self, value):
-        self.__ldap_uri = value
-
-    @property
-    def base_dn(self):
-        try:
-            if self.__base_dn is not None:
-                return DN(self.__base_dn)
-            else:
-                return DN(self.api.env.basedn)
-        except AttributeError:
-            return DN()
+        CrudBackend.__init__(self, api)
+        LDAPClient.__init__(self, ldap_uri,
+                            force_schema_updates=force_schema_updates)
 
     def _connect(self):
         # Connectible.conn is a proxy to thread-local storage;
@@ -145,8 +118,6 @@ class ldap2(LDAPClient, CrudBackend):
         if debug_level:
             _ldap.set_option(_ldap.OPT_DEBUG_LEVEL, debug_level)
 
-        object.__setattr__(self, '_force_schema_updates',
-                           self.api.env.context in ('installer', 'updates'))
         LDAPClient._connect(self)
         conn = self._conn
 
diff --git a/ipaserver/plugins/rabase.py b/ipaserver/plugins/rabase.py
index cf44262..fae30ff 100644
--- a/ipaserver/plugins/rabase.py
+++ b/ipaserver/plugins/rabase.py
@@ -41,14 +41,14 @@ class rabase(Backend):
     """
     Request Authority backend plugin.
     """
-    def __init__(self):
+    def __init__(self, api):
         if api.env.in_tree:
             self.sec_dir = api.env.dot_ipa + os.sep + 'alias'
             self.pwd_file = self.sec_dir + os.sep + '.pwd'
         else:
             self.sec_dir = paths.HTTPD_ALIAS_DIR
             self.pwd_file = paths.ALIAS_PWDFILE_TXT
-        super(rabase, self).__init__()
+        super(rabase, self).__init__(api)
 
 
     def check_request_status(self, request_id):
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index 43ee63e..ead830d 100644
--- a/ipaserver/rpcserver.py
+++ b/ipaserver/rpcserver.py
@@ -238,8 +238,8 @@ class wsgi_dispatch(Executioner, HTTP_Status):
     handler which is specific to the authentication and RPC mechanism.
     """
 
-    def __init__(self):
-        super(wsgi_dispatch, self).__init__()
+    def __init__(self, api):
+        super(wsgi_dispatch, self).__init__(api)
         self.__apps = {}
 
     def __iter__(self):
@@ -301,14 +301,11 @@ class WSGIExecutioner(Executioner):
 
     _system_commands = {}
 
-    def set_api(self, api):
-        super(WSGIExecutioner, self).set_api(api)
-        if 'wsgi_dispatch' in self.api.Backend:
-            self.api.Backend.wsgi_dispatch.mount(self, self.key)
-
     def _on_finalize(self):
         self.url = self.env.mount_ipa + self.key
         super(WSGIExecutioner, self)._on_finalize()
+        if 'wsgi_dispatch' in self.api.Backend:
+            self.api.Backend.wsgi_dispatch.mount(self, self.key)
 
     def wsgi_execute(self, environ):
         result = None
@@ -746,8 +743,8 @@ class jsonserver_session(jsonserver, KerberosSession):
 
     key = '/session/json'
 
-    def __init__(self):
-        super(jsonserver_session, self).__init__()
+    def __init__(self, api):
+        super(jsonserver_session, self).__init__(api)
         name = '{0}_{1}'.format(self.__class__.__name__, id(self))
         auth_mgr = AuthManagerKerb(name)
         session_mgr.auth_mgr.register(auth_mgr.name, auth_mgr)
@@ -849,9 +846,6 @@ class jsonserver_kerb(jsonserver, KerberosWSGIExecutioner):
 class login_kerberos(Backend, KerberosSession, HTTP_Status):
     key = '/session/login_kerberos'
 
-    def __init__(self):
-        super(login_kerberos, self).__init__()
-
     def _on_finalize(self):
         super(login_kerberos, self)._on_finalize()
         self.api.Backend.wsgi_dispatch.mount(self, self.key)
@@ -873,9 +867,6 @@ class login_password(Backend, KerberosSession, HTTP_Status):
     content_type = 'text/plain'
     key = '/session/login_password'
 
-    def __init__(self):
-        super(login_password, self).__init__()
-
     def _on_finalize(self):
         super(login_password, self)._on_finalize()
         self.api.Backend.wsgi_dispatch.mount(self, self.key)
@@ -998,9 +989,6 @@ class change_password(Backend, HTTP_Status):
     content_type = 'text/plain'
     key = '/session/change_password'
 
-    def __init__(self):
-        super(change_password, self).__init__()
-
     def _on_finalize(self):
         super(change_password, self)._on_finalize()
         self.api.Backend.wsgi_dispatch.mount(self, self.key)
@@ -1051,8 +1039,7 @@ class change_password(Backend, HTTP_Status):
             pw = data['old_password']
             if data.get('otp'):
                 pw = data['old_password'] + data['otp']
-            conn = ldap2(shared_instance=False,
-                         ldap_uri=self.api.env.ldap_uri)
+            conn = ldap2(self.api)
             conn.connect(bind_dn=bind_dn, bind_pw=pw)
         except (NotFound, ACIError):
             result = 'invalid-password'
@@ -1104,9 +1091,6 @@ class sync_token(Backend, HTTP_Status):
             namedtype.OptionalNamedType('tokenDN', univ.OctetString())
         )
 
-    def __init__(self):
-        super(sync_token, self).__init__()
-
     def _on_finalize(self):
         super(sync_token, self)._on_finalize()
         self.api.Backend.wsgi_dispatch.mount(self, self.key)
@@ -1165,7 +1149,7 @@ class sync_token(Backend, HTTP_Status):
         title = 'Token sync rejected'
 
         # Perform the synchronization.
-        conn = ldap2(shared_instance=False, ldap_uri=self.api.env.ldap_uri)
+        conn = ldap2(self.api)
         try:
             conn.connect(bind_dn=bind_dn,
                          bind_pw=data['password'],
@@ -1199,8 +1183,8 @@ class xmlserver_session(xmlserver, KerberosSession):
 
     key = '/session/xml'
 
-    def __init__(self):
-        super(xmlserver_session, self).__init__()
+    def __init__(self, api):
+        super(xmlserver_session, self).__init__(api)
         name = '{0}_{1}'.format(self.__class__.__name__, id(self))
         auth_mgr = AuthManagerKerb(name)
         session_mgr.auth_mgr.register(auth_mgr.name, auth_mgr)
diff --git a/ipatests/test_ipalib/test_backend.py b/ipatests/test_ipalib/test_backend.py
index 91eb920..163c4f2 100644
--- a/ipatests/test_ipalib/test_backend.py
+++ b/ipatests/test_ipalib/test_backend.py
@@ -71,12 +71,13 @@ class test_Connectible(ClassChecker):
         Test the `ipalib.backend.Connectible.connect` method.
         """
         # Test that connection is created:
+        api = 'the api instance'
         class example(self.cls):
             def create_connection(self, *args, **kw):
                 object.__setattr__(self, 'args', args)
                 object.__setattr__(self, 'kw', kw)
                 return 'The connection.'
-        o = example(shared_instance=True)
+        o = example(api, shared_instance=True)
         args = ('Arg1', 'Arg2', 'Arg3')
         kw = dict(key1='Val1', key2='Val2', key3='Val3')
         assert not hasattr(context, 'example')
@@ -101,10 +102,11 @@ class test_Connectible(ClassChecker):
         """
         Test the `ipalib.backend.Connectible.create_connection` method.
         """
+        api = 'the api instance'
         class example(self.cls):
             pass
         for klass in (self.cls, example):
-            o = klass(shared_instance=True)
+            o = klass(api, shared_instance=True)
             e = raises(NotImplementedError, o.create_connection)
             assert str(e) == '%s.create_connection()' % klass.__name__
 
@@ -112,9 +114,10 @@ class test_Connectible(ClassChecker):
         """
         Test the `ipalib.backend.Connectible.disconnect` method.
         """
+        api = 'the api instance'
         class example(self.cls):
             destroy_connection = Disconnect()
-        o = example(shared_instance=True)
+        o = example(api, shared_instance=True)
 
         m = "disconnect: 'context.%s' does not exist in thread %r"
         e = raises(StandardError, o.disconnect)
@@ -128,10 +131,11 @@ class test_Connectible(ClassChecker):
         """
         Test the `ipalib.backend.Connectible.destroy_connection` method.
         """
+        api = 'the api instance'
         class example(self.cls):
             pass
         for klass in (self.cls, example):
-            o = klass(shared_instance=True)
+            o = klass(api, shared_instance=True)
             e = raises(NotImplementedError, o.destroy_connection)
             assert str(e) == '%s.destroy_connection()' % klass.__name__
 
@@ -139,10 +143,11 @@ class test_Connectible(ClassChecker):
         """
         Test the `ipalib.backend.Connectible.isconnected` method.
         """
+        api = 'the api instance'
         class example(self.cls):
             pass
         for klass in (self.cls, example):
-            o = klass(shared_instance=True)
+            o = klass(api, shared_instance=True)
             assert o.isconnected() is False
             conn = 'whatever'
             setattr(context, klass.__name__, conn)
@@ -153,11 +158,12 @@ class test_Connectible(ClassChecker):
         """
         Test the `ipalib.backend.Connectible.conn` property.
         """
+        api = 'the api instance'
         msg = 'no context.%s in thread %r'
         class example(self.cls):
             pass
         for klass in (self.cls, example):
-            o = klass(shared_instance=True)
+            o = klass(api, shared_instance=True)
             e = raises(AttributeError, getattr, o, 'conn')
             assert str(e) == msg % (
                 klass.__name__, threading.currentThread().getName()
@@ -211,8 +217,7 @@ class test_Executioner(ClassChecker):
         api.add_plugin(with_name)
 
         api.finalize()
-        o = self.cls()
-        o.set_api(api)
+        o = self.cls(api)
         o.finalize()
 
         # Test that CommandError is raised:
diff --git a/ipatests/test_ipalib/test_cli.py b/ipatests/test_ipalib/test_cli.py
index 07935c5..4c9ae61 100644
--- a/ipatests/test_ipalib/test_cli.py
+++ b/ipatests/test_ipalib/test_cli.py
@@ -32,7 +32,8 @@ class test_textui(ClassChecker):
         """
         Test the `ipalib.cli.textui.max_col_width` method.
         """
-        o = self.cls()
+        api = 'the api instance'
+        o = self.cls(api)
         e = raises(TypeError, o.max_col_width, 'hello')
         assert str(e) == 'rows: need %r or %r; got %r' % (list, tuple, 'hello')
         rows = [
diff --git a/ipatests/test_ipalib/test_crud.py b/ipatests/test_ipalib/test_crud.py
index f173716..910da27 100644
--- a/ipatests/test_ipalib/test_crud.py
+++ b/ipatests/test_ipalib/test_crud.py
@@ -202,10 +202,11 @@ class test_CrudBackend(ClassChecker):
         return ldap
 
     def check_method(self, name, *args):
-        o = self.cls()
+        api = 'the api instance'
+        o = self.cls(api)
         e = raises(NotImplementedError, getattr(o, name), *args)
         assert str(e) == 'CrudBackend.%s()' % name
-        sub = self.subcls()
+        sub = self.subcls(api)
         e = raises(NotImplementedError, getattr(sub, name), *args)
         assert str(e) == 'ldap.%s()' % name
 
diff --git a/ipatests/test_ipalib/test_frontend.py b/ipatests/test_ipalib/test_frontend.py
index c47113f..1e27dfe 100644
--- a/ipatests/test_ipalib/test_frontend.py
+++ b/ipatests/test_ipalib/test_frontend.py
@@ -89,31 +89,32 @@ class test_HasParam(ClassChecker):
         """
         Test the `ipalib.frontend.HasParam._get_param_iterable` method.
         """
+        api = 'the api instance'
         class WithTuple(self.cls):
             takes_stuff = ('one', 'two')
-        o = WithTuple()
+        o = WithTuple(api)
         assert o._get_param_iterable('stuff') is WithTuple.takes_stuff
 
         junk = ('three', 'four')
         class WithCallable(self.cls):
             def takes_stuff(self):
                 return junk
-        o = WithCallable()
+        o = WithCallable(api)
         assert o._get_param_iterable('stuff') is junk
 
         class WithParam(self.cls):
             takes_stuff = parameters.Str('five')
-        o = WithParam()
+        o = WithParam(api)
         assert o._get_param_iterable('stuff') == (WithParam.takes_stuff,)
 
         class WithStr(self.cls):
             takes_stuff = 'six'
-        o = WithStr()
+        o = WithStr(api)
         assert o._get_param_iterable('stuff') == ('six',)
 
         class Wrong(self.cls):
             takes_stuff = ['seven', 'eight']
-        o = Wrong()
+        o = Wrong(api)
         e = raises(TypeError, o._get_param_iterable, 'stuff')
         assert str(e) == '%s.%s must be a tuple, callable, or spec; got %r' % (
             'Wrong', 'takes_stuff', Wrong.takes_stuff
@@ -123,6 +124,7 @@ class test_HasParam(ClassChecker):
         """
         Test the `ipalib.frontend.HasParam._filter_param_by_context` method.
         """
+        api = 'the api instance'
         class Example(self.cls):
             def get_stuff(self):
                 return (
@@ -132,7 +134,7 @@ class test_HasParam(ClassChecker):
                     parameters.Str('four', exclude='server'),
                     parameters.Str('five', exclude=['whatever', 'cli']),
                 )
-        o = Example()
+        o = Example(api)
 
         # Test when env is None:
         params = list(o._filter_param_by_context('stuff'))
@@ -161,7 +163,7 @@ class test_HasParam(ClassChecker):
         # Test with no get_stuff:
         class Missing(self.cls):
             pass
-        o = Missing()
+        o = Missing(api)
         gen = o._filter_param_by_context('stuff')
         e = raises(NotImplementedError, list, gen)
         assert str(e) == 'Missing.get_stuff()'
@@ -169,7 +171,7 @@ class test_HasParam(ClassChecker):
         # Test when get_stuff is not callable:
         class NotCallable(self.cls):
             get_stuff = ('one', 'two')
-        o = NotCallable()
+        o = NotCallable(api)
         gen = o._filter_param_by_context('stuff')
         e = raises(TypeError, list, gen)
         assert str(e) == '%s.%s must be a callable; got %r' % (
@@ -219,10 +221,11 @@ class test_Command(ClassChecker):
         """
         Helper method used to test args and options.
         """
+        api = 'the api instance'
         class example(self.cls):
             takes_args = args
             takes_options = options
-        o = example()
+        o = example(api)
         o.finalize()
         return o
 
@@ -237,7 +240,8 @@ class test_Command(ClassChecker):
         """
         Test the `ipalib.frontend.Command.get_args` method.
         """
-        assert list(self.cls().get_args()) == []
+        api = 'the api instance'
+        assert list(self.cls(api).get_args()) == []
         args = ('login', 'stuff')
         o = self.get_instance(args=args)
         assert tuple(o.get_args()) == args
@@ -246,7 +250,8 @@ class test_Command(ClassChecker):
         """
         Test the `ipalib.frontend.Command.get_options` method.
         """
-        options = list(self.cls().get_options())
+        api = 'the api instance'
+        options = list(self.cls(api).get_options())
         assert len(options) == 1
         assert options[0].name == 'version'
         options = ('verbose', 'debug')
@@ -259,8 +264,8 @@ class test_Command(ClassChecker):
         """
         Test the ``ipalib.frontend.Command.args`` instance attribute.
         """
-        assert self.cls().args is None
-        o = self.cls()
+        api = 'the api instance'
+        o = self.cls(api)
         o.finalize()
         assert type(o.args) is plugable.NameSpace
         assert len(o.args) == 0
@@ -308,8 +313,8 @@ class test_Command(ClassChecker):
         """
         Test the ``ipalib.frontend.Command.options`` instance attribute.
         """
-        assert self.cls().options is None
-        o = self.cls()
+        api = 'the api instance'
+        o = self.cls(api)
         o.finalize()
         assert type(o.options) is plugable.NameSpace
         assert len(o.options) == 1
@@ -329,8 +334,8 @@ class test_Command(ClassChecker):
         """
         Test the ``ipalib.frontend.Command.output`` instance attribute.
         """
-        inst = self.cls()
-        assert inst.output is None
+        api = 'the api instance'
+        inst = self.cls(api)
         inst.finalize()
         assert type(inst.output) is plugable.NameSpace
         assert list(inst.output) == ['result']
@@ -340,9 +345,10 @@ class test_Command(ClassChecker):
         """
         Test the ``ipalib.frontend.Command._iter_output`` instance attribute.
         """
+        api = 'the api instance'
         class Example(self.cls):
             pass
-        inst = Example()
+        inst = Example(api)
 
         inst.has_output = tuple()
         assert list(inst._iter_output()) == []
@@ -373,6 +379,8 @@ class test_Command(ClassChecker):
         """
         Test the `ipalib.frontend.Command.soft_validate` method.
         """
+        class api(object):
+            env = config.Env(context='cli')
         class user_add(frontend.Command):
             takes_args = parameters.Str('uid',
                 normalizer=lambda value: value.lower(),
@@ -381,8 +389,7 @@ class test_Command(ClassChecker):
 
             takes_options = ('givenname', 'sn')
 
-        cmd = user_add()
-        cmd.env = config.Env(context='cli')
+        cmd = user_add(api)
         cmd.finalize()
         assert list(cmd.params) == ['givenname', 'sn', 'uid', 'version']
         ret = cmd.soft_validate({})
@@ -398,11 +405,12 @@ class test_Command(ClassChecker):
         """
         Test the `ipalib.frontend.Command.convert` method.
         """
+        api = 'the api instance'
         kw = dict(
             option0=u'1.5',
             option1=u'7',
         )
-        o = self.subcls()
+        o = self.subcls(api)
         o.finalize()
         for (key, value) in o.convert(**kw).iteritems():
             assert_equal(unicode(kw[key]), value)
@@ -411,12 +419,13 @@ class test_Command(ClassChecker):
         """
         Test the `ipalib.frontend.Command.normalize` method.
         """
+        api = 'the api instance'
         kw = dict(
             option0=u'OPTION0',
             option1=u'OPTION1',
         )
         norm = dict((k, v.lower()) for (k, v) in kw.items())
-        sub = self.subcls()
+        sub = self.subcls(api)
         sub.finalize()
         assert sub.normalize(**kw) == norm
 
@@ -444,8 +453,7 @@ class test_Command(ClassChecker):
 
         (api, home) = create_test_api()
         api.finalize()
-        o = my_cmd()
-        o.set_api(api)
+        o = my_cmd(api)
         o.finalize()
         e = o(**kw)  # pylint: disable=not-callable
         assert type(e) is dict
@@ -457,9 +465,10 @@ class test_Command(ClassChecker):
         """
         Test the `ipalib.frontend.Command.validate` method.
         """
+        class api(object):
+            env = config.Env(context='cli')
 
-        sub = self.subcls()
-        sub.env = config.Env(context='cli')
+        sub = self.subcls(api)
         sub.finalize()
 
         # Check with valid values
@@ -491,7 +500,8 @@ class test_Command(ClassChecker):
         """
         Test the `ipalib.frontend.Command.execute` method.
         """
-        o = self.cls()
+        api = 'the api instance'
+        o = self.cls(api)
         e = raises(NotImplementedError, o.execute)
         assert str(e) == 'Command.execute()'
 
@@ -568,8 +578,7 @@ class test_Command(ClassChecker):
 
         (api, home) = create_test_api()
         api.finalize()
-        o = my_cmd()
-        o.set_api(api)
+        o = my_cmd(api)
         o.finalize()
         e = o.run(*args, **kw)
         assert type(e) is dict
@@ -608,8 +617,7 @@ class test_Command(ClassChecker):
         # Test in server context:
         (api, home) = create_test_api(in_server=True)
         api.finalize()
-        o = my_cmd()
-        o.set_api(api)
+        o = my_cmd(api)
         assert o.run.im_func is self.cls.run.im_func
         out = o.run(*args, **kw)
         assert ('execute', args, kw) == out
@@ -617,8 +625,7 @@ class test_Command(ClassChecker):
         # Test in non-server context
         (api, home) = create_test_api(in_server=False)
         api.finalize()
-        o = my_cmd()
-        o.set_api(api)
+        o = my_cmd(api)
         assert o.run.im_func is self.cls.run.im_func
         assert ('forward', args, kw) == o.run(*args, **kw)
 
@@ -650,16 +657,14 @@ class test_Command(ClassChecker):
         # Test in server context:
         (api, home) = create_test_api(in_server=True)
         api.finalize()
-        o = my_cmd()
-        o.set_api(api)
+        o = my_cmd(api)
         assert o.run.im_func is self.cls.run.im_func
         assert {'name': 'execute', 'messages': expected} == o.run(*args, **kw)
 
         # Test in non-server context
         (api, home) = create_test_api(in_server=False)
         api.finalize()
-        o = my_cmd()
-        o.set_api(api)
+        o = my_cmd(api)
         assert o.run.im_func is self.cls.run.im_func
         assert {'name': 'forward', 'messages': expected} == o.run(*args, **kw)
 
@@ -667,10 +672,11 @@ class test_Command(ClassChecker):
         """
         Test the `ipalib.frontend.Command.validate_output` method.
         """
+        api = 'the api instance'
         class Example(self.cls):
             has_output = ('foo', 'bar', 'baz')
 
-        inst = Example()
+        inst = Example(api)
         inst.finalize()
 
         # Test with wrong type:
@@ -705,13 +711,14 @@ class test_Command(ClassChecker):
         """
         Test `ipalib.frontend.Command.validate_output` per-type validation.
         """
+        api = 'the api instance'
 
         class Complex(self.cls):
             has_output = (
                 output.Output('foo', int),
                 output.Output('bar', list),
             )
-        inst = Complex()
+        inst = Complex(api)
         inst.finalize()
 
         wrong = dict(foo=17.9, bar=[18])
@@ -730,6 +737,7 @@ class test_Command(ClassChecker):
         """
         Test `ipalib.frontend.Command.validate_output` nested validation.
         """
+        api = 'the api instance'
 
         class Subclass(output.ListOfEntries):
             pass
@@ -740,7 +748,7 @@ class test_Command(ClassChecker):
                 output.Output('hello', int),
                 Subclass('world'),
             )
-        inst = nested()
+        inst = nested(api)
         inst.finalize()
         okay = dict(foo='bar')
         nope = ('aye', 'bee')
@@ -761,6 +769,7 @@ class test_Command(ClassChecker):
         """
         Test the `ipalib.frontend.Command.get_output_params` method.
         """
+        api = 'the api instance'
         class example(self.cls):
             has_output_params = (
                 'one',
@@ -775,8 +784,7 @@ class test_Command(ClassChecker):
                 'baz',
             )
 
-        inst = example()
-        assert list(inst.get_output_params()) == ['one', 'two', 'three']
+        inst = example(api)
         inst.finalize()
         assert list(inst.get_output_params()) == [
             'one', 'two', 'three', inst.params.foo, inst.params.baz
@@ -794,7 +802,8 @@ class test_LocalOrRemote(ClassChecker):
         """
         Test the `ipalib.frontend.LocalOrRemote.__init__` method.
         """
-        o = self.cls()
+        api = 'the api instance'
+        o = self.cls(api)
         o.finalize()
         assert list(o.args) == []
         assert list(o.options) == ['server', 'version']
@@ -872,16 +881,6 @@ class test_Object(ClassChecker):
         """
         Test the `ipalib.frontend.Object.__init__` method.
         """
-        o = self.cls()
-        assert o.backend is None
-        assert o.methods is None
-        assert o.params is None
-        assert o.params_minus_pk is None
-
-    def test_set_api(self):
-        """
-        Test the `ipalib.frontend.Object.set_api` method.
-        """
         # Setup for test:
         class DummyAttribute(object):
             def __init__(self, obj_name, attr_name, name=None):
@@ -908,20 +907,22 @@ class test_Object(ClassChecker):
         cnt = 10
         methods_format = 'method_%d'
 
-        _d = dict(
-            Method=plugable.NameSpace(
+        class FakeAPI(object):
+            Method = plugable.NameSpace(
                 get_attributes(cnt, methods_format)
-            ),
-        )
-        api = plugable.MagicDict(_d)
+            )
+            def __contains__(self, key):
+                return hasattr(self, key)
+            def __getitem__(self, key):
+                return getattr(self, key)
+        api = FakeAPI()
         assert len(api.Method) == cnt * 3
 
         class user(self.cls):
             pass
 
         # Actually perform test:
-        o = user()
-        o.set_api(api)
+        o = user(api)
         assert read_only(o, 'api') is api
 
         namespace = o.methods
@@ -938,15 +939,13 @@ class test_Object(ClassChecker):
             assert attr.name == '%s_%s' % ('user', attr_name)
 
         # Test params instance attribute
-        o = self.cls()
-        o.set_api(api)
+        o = self.cls(api)
         ns = o.params
         assert type(ns) is plugable.NameSpace
         assert len(ns) == 0
         class example(self.cls):
             takes_params = ('banana', 'apple')
-        o = example()
-        o.set_api(api)
+        o = example(api)
         ns = o.params
         assert type(ns) is plugable.NameSpace
         assert len(ns) == 2, repr(ns)
@@ -969,8 +968,7 @@ class test_Object(ClassChecker):
                 'one',
                 'two',
             )
-        o = example1()
-        o.set_api(api)
+        o = example1(api)
         assert o.primary_key is None
 
         # Test with 1 primary key:
@@ -981,8 +979,7 @@ class test_Object(ClassChecker):
                 parameters.Str('three', primary_key=True),
                 'four',
             )
-        o = example2()
-        o.set_api(api)
+        o = example2(api)
         pk = o.primary_key
         assert type(pk) is parameters.Str
         assert pk.name == 'three'
@@ -999,8 +996,7 @@ class test_Object(ClassChecker):
                 'three',
                 parameters.Str('four', primary_key=True),
             )
-        o = example3()
-        o.set_api(api)
+        o = example3(api)
         e = raises(ValueError, o.finalize)
         assert str(e) == \
             'example3 (Object) has multiple primary keys: one, two, four'
@@ -1025,12 +1021,13 @@ class test_Object(ClassChecker):
         """
         Test the `ipalib.frontend.Object.get_dn` method.
         """
-        o = self.cls()
+        api = 'the api instance'
+        o = self.cls(api)
         e = raises(NotImplementedError, o.get_dn, 'primary key')
         assert str(e) == 'Object.get_dn()'
         class user(self.cls):
             pass
-        o = user()
+        o = user(api)
         e = raises(NotImplementedError, o.get_dn, 'primary key')
         assert str(e) == 'user.get_dn()'
 
@@ -1040,9 +1037,9 @@ class test_Object(ClassChecker):
         """
         class example(self.cls):
             takes_params = ('one', 'two', 'three', 'four')
-        o = example()
         (api, home) = create_test_api()
-        o.set_api(api)
+        api.finalize()
+        o = example(api)
         p = o.params
         assert tuple(o.params_minus()) == tuple(p())
         assert tuple(o.params_minus([])) == tuple(p())
@@ -1073,28 +1070,16 @@ class test_Attribute(ClassChecker):
         """
         Test the `ipalib.frontend.Attribute.__init__` method.
         """
-        class user_add(self.cls):
-            pass
-        o = user_add()
-        assert read_only(o, 'obj') is None
-        assert read_only(o, 'obj_name') == 'user'
-        assert read_only(o, 'attr_name') == 'add'
-
-    def test_set_api(self):
-        """
-        Test the `ipalib.frontend.Attribute.set_api` method.
-        """
         user_obj = 'The user frontend.Object instance'
         class api(object):
             Object = dict(user=user_obj)
         class user_add(self.cls):
             pass
-        o = user_add()
-        assert read_only(o, 'api') is None
-        assert read_only(o, 'obj') is None
-        o.set_api(api)
+        o = user_add(api)
         assert read_only(o, 'api') is api
         assert read_only(o, 'obj') is user_obj
+        assert read_only(o, 'obj_name') == 'user'
+        assert read_only(o, 'attr_name') == 'add'
 
 
 class test_Method(ClassChecker):
@@ -1133,9 +1118,10 @@ class test_Method(ClassChecker):
         """
         Test the `ipalib.frontend.Method.__init__` method.
         """
+        api = 'the api instance'
         class user_add(self.cls):
             pass
-        o = user_add()
+        o = user_add(api)
         assert o.name == 'user_add'
         assert o.obj_name == 'user'
         assert o.attr_name == 'add'
diff --git a/ipatests/test_ipalib/test_output.py b/ipatests/test_ipalib/test_output.py
index e722a97..1161e64 100644
--- a/ipatests/test_ipalib/test_output.py
+++ b/ipatests/test_ipalib/test_output.py
@@ -71,9 +71,10 @@ class test_ListOfEntries(ClassChecker):
         """
         Test the `ipalib.output.ListOfEntries.validate` method.
         """
+        api = 'the api instance'
         class example(Command):
             pass
-        cmd = example()
+        cmd = example(api)
         inst = self.cls('stuff')
 
         okay = dict(foo='bar')
diff --git a/ipatests/test_ipalib/test_plugable.py b/ipatests/test_ipalib/test_plugable.py
index b0f6070..0d2763d 100644
--- a/ipatests/test_ipalib/test_plugable.py
+++ b/ipatests/test_ipalib/test_plugable.py
@@ -219,7 +219,8 @@ class test_Plugin(ClassChecker):
         """
         Test the `ipalib.plugable.Plugin.__init__` method.
         """
-        o = self.cls()
+        api = 'the api instance'
+        o = self.cls(api)
         assert o.name == 'Plugin'
         assert o.module == 'ipalib.plugable'
         assert o.fullname == 'ipalib.plugable.Plugin'
@@ -234,7 +235,7 @@ class test_Plugin(ClassChecker):
 
             One more paragraph.
             """
-        o = some_subclass()
+        o = some_subclass(api)
         assert o.name == 'some_subclass'
         assert o.module == __name__
         assert o.fullname == '%s.some_subclass' % __name__
@@ -242,36 +243,23 @@ class test_Plugin(ClassChecker):
         assert isinstance(o.doc, text.Gettext)
         class another_subclass(self.cls):
             pass
-        o = another_subclass()
+        o = another_subclass(api)
         assert o.summary == '<%s>' % o.fullname
 
         # Test that Plugin makes sure the subclass hasn't defined attributes
         # whose names conflict with the logger methods set in Plugin.__init__():
         class check(self.cls):
             info = 'whatever'
-        e = raises(StandardError, check)
+        e = raises(StandardError, check, api)
         assert str(e) == \
             "info is already bound to ipatests.test_ipalib.test_plugable.check()"
 
-    def test_set_api(self):
-        """
-        Test the `ipalib.plugable.Plugin.set_api` method.
-        """
-        api = 'the api instance'
-        o = self.cls()
-        assert o.api is None
-        e = raises(AssertionError, o.set_api, None)
-        assert str(e) == 'set_api() argument cannot be None'
-        o.set_api(api)
-        assert o.api is api
-        e = raises(AssertionError, o.set_api, api)
-        assert str(e) == 'set_api() can only be called once'
-
     def test_finalize(self):
         """
         Test the `ipalib.plugable.Plugin.finalize` method.
         """
-        o = self.cls()
+        api = 'the api instance'
+        o = self.cls(api)
         assert not o.__islocked__()
         o.finalize()
         assert o.__islocked__()
diff --git a/ipatests/test_ipaserver/test_ldap.py b/ipatests/test_ipaserver/test_ldap.py
index 2c187b0..05ae87c 100644
--- a/ipatests/test_ipaserver/test_ldap.py
+++ b/ipatests/test_ipaserver/test_ldap.py
@@ -60,7 +60,7 @@ class test_ldap(object):
         """
         Test an anonymous LDAP bind using ldap2
         """
-        self.conn = ldap2(shared_instance=False, ldap_uri=self.ldapuri)
+        self.conn = ldap2(api, ldap_uri=self.ldapuri)
         self.conn.connect()
         dn = api.env.basedn
         entry_attrs = self.conn.get_entry(dn, ['associateddomain'])
@@ -73,7 +73,7 @@ class test_ldap(object):
         """
         if not ipautil.file_exists(self.ccache):
             raise nose.SkipTest('Missing ccache %s' % self.ccache)
-        self.conn = ldap2(shared_instance=False, ldap_uri=self.ldapuri)
+        self.conn = ldap2(api, ldap_uri=self.ldapuri)
         self.conn.connect(ccache='FILE:%s' % self.ccache)
         entry_attrs = self.conn.get_entry(self.dn, ['usercertificate'])
         cert = entry_attrs.get('usercertificate')
@@ -92,7 +92,7 @@ class test_ldap(object):
             fp.close()
         else:
             raise nose.SkipTest("No directory manager password in %s" % pwfile)
-        self.conn = ldap2(shared_instance=False, ldap_uri=self.ldapuri)
+        self.conn = ldap2(api, ldap_uri=self.ldapuri)
         self.conn.connect(bind_dn=DN(('cn', 'directory manager')), bind_pw=dm_password)
         entry_attrs = self.conn.get_entry(self.dn, ['usercertificate'])
         cert = entry_attrs.get('usercertificate')
@@ -137,7 +137,7 @@ class test_ldap(object):
         Test an autobind LDAP bind using ldap2
         """
         ldapuri = 'ldapi://%%2fvar%%2frun%%2fslapd-%s.socket' % api.env.realm.replace('.','-')
-        self.conn = ldap2(shared_instance=False, ldap_uri=ldapuri)
+        self.conn = ldap2(api, ldap_uri=ldapuri)
         try:
             self.conn.connect(autobind=True)
         except errors.ACIError:
@@ -160,7 +160,7 @@ class test_LDAPEntry(object):
 
     def setup(self):
         self.ldapuri = 'ldap://%s' % ipautil.format_netloc(api.env.host)
-        self.conn = ldap2(shared_instance=False, ldap_uri=self.ldapuri)
+        self.conn = ldap2(api, ldap_uri=self.ldapuri)
         self.conn.connect()
 
         self.entry = self.conn.make_entry(self.dn1, cn=self.cn1)
diff --git a/ipatests/test_ipaserver/test_rpcserver.py b/ipatests/test_ipaserver/test_rpcserver.py
index 08d773c..c77c2d9 100644
--- a/ipatests/test_ipaserver/test_rpcserver.py
+++ b/ipatests/test_ipaserver/test_rpcserver.py
@@ -47,7 +47,8 @@ class StartResponse(object):
 
 
 def test_not_found():
-    f = rpcserver.HTTP_Status()
+    api = 'the api instance'
+    f = rpcserver.HTTP_Status(api)
     t = rpcserver._not_found_template
     s = StartResponse()
 
@@ -72,7 +73,8 @@ def test_not_found():
 
 
 def test_bad_request():
-    f = rpcserver.HTTP_Status()
+    api = 'the api instance'
+    f = rpcserver.HTTP_Status(api)
     t = rpcserver._bad_request_template
     s = StartResponse()
 
@@ -85,7 +87,8 @@ def test_bad_request():
 
 
 def test_internal_error():
-    f = rpcserver.HTTP_Status()
+    api = 'the api instance'
+    f = rpcserver.HTTP_Status(api)
     t = rpcserver._internal_error_template
     s = StartResponse()
 
@@ -98,7 +101,8 @@ def test_internal_error():
 
 
 def test_unauthorized_error():
-    f = rpcserver.HTTP_Status()
+    api = 'the api instance'
+    f = rpcserver.HTTP_Status(api)
     t = rpcserver._unauthorized_template
     s = StartResponse()
 
@@ -139,7 +143,8 @@ class test_session(object):
                 [environ[k] for k in ('SCRIPT_NAME', 'PATH_INFO')]
             )
 
-        inst = self.klass()
+        api = 'the api instance'
+        inst = self.klass(api)
         inst.mount(app1, '/foo/stuff')
         inst.mount(app2, '/bar')
 
@@ -157,7 +162,8 @@ class test_session(object):
             pass
 
         # Test that mount works:
-        inst = self.klass()
+        api = 'the api instance'
+        inst = self.klass(api)
         inst.mount(app1, 'foo')
         assert inst['foo'] is app1
         assert list(inst) == ['foo']
diff --git a/ipatests/test_xmlrpc/test_baseldap_plugin.py b/ipatests/test_xmlrpc/test_baseldap_plugin.py
index 6b19e57..6da5626 100644
--- a/ipatests/test_xmlrpc/test_baseldap_plugin.py
+++ b/ipatests/test_xmlrpc/test_baseldap_plugin.py
@@ -44,7 +44,8 @@ def test_exc_wrapper():
             assert kwargs == dict(a=1, b=2)
             raise errors.ExecutionError('failure')
 
-    instance = test_callback()
+    api = 'the api instance'
+    instance = test_callback(api)
 
     # Test with one callback first
 
@@ -96,8 +97,10 @@ def test_callback_registration():
     callbacktest_subclass.register_callback('test', subclass_callback)
 
 
+    api = 'the api instance'
+
     messages = []
-    instance = callbacktest_base()
+    instance = callbacktest_base(api)
     for callback in instance.get_callbacks('test'):
         callback(instance, 42)
     assert messages == [
@@ -106,7 +109,7 @@ def test_callback_registration():
             ('Registered callback from another class', 42)]
 
     messages = []
-    instance = callbacktest_subclass()
+    instance = callbacktest_subclass(api)
     for callback in instance.get_callbacks('test'):
         callback(instance, 42)
     assert messages == [
@@ -134,7 +137,9 @@ def test_exc_callback_registration():
             """Raise an error"""
             raise errors.ExecutionError('failure')
 
-    base_instance = callbacktest_base()
+    api = 'the api instance'
+
+    base_instance = callbacktest_base(api)
 
     class callbacktest_subclass(callbacktest_base):
         pass
@@ -145,7 +150,7 @@ def test_exc_callback_registration():
         messages.append('Subclass registered callback')
         raise exc
 
-    subclass_instance = callbacktest_subclass()
+    subclass_instance = callbacktest_subclass(api)
 
     # Make sure exception in base class is only handled by the base class
     base_instance.test_fail()
diff --git a/ipatests/test_xmlrpc/test_dns_plugin.py b/ipatests/test_xmlrpc/test_dns_plugin.py
index e38ea42..83b5b2a 100644
--- a/ipatests/test_xmlrpc/test_dns_plugin.py
+++ b/ipatests/test_xmlrpc/test_dns_plugin.py
@@ -400,7 +400,7 @@ def _get_nameservers_ldap(conn):
 
 
 def get_nameservers():
-        ldap = ldap2(shared_instance=False)
+        ldap = ldap2(api)
         ldap.connect(ccache=krbV.default_context().default_ccache())
         nameservers = [normalize_zone(x) for x in _get_nameservers_ldap(ldap)]
         return nameservers
diff --git a/ipatests/test_xmlrpc/test_permission_plugin.py b/ipatests/test_xmlrpc/test_permission_plugin.py
index 4503b0d..c899c42 100644
--- a/ipatests/test_xmlrpc/test_permission_plugin.py
+++ b/ipatests/test_xmlrpc/test_permission_plugin.py
@@ -3174,7 +3174,7 @@ class test_managed_permissions(Declarative):
 
     def add_managed_permission(self):
         """Add a managed permission and the corresponding ACI"""
-        ldap = ldap2(shared_instance=False)
+        ldap = ldap2(api)
         ldap.connect(ccache=krbV.default_context().default_ccache())
 
         result = api.Command.permission_add(permission1, type=u'user',
diff --git a/ipatests/test_xmlrpc/testcert.py b/ipatests/test_xmlrpc/testcert.py
index 4afd38d..b9a0111 100644
--- a/ipatests/test_xmlrpc/testcert.py
+++ b/ipatests/test_xmlrpc/testcert.py
@@ -74,7 +74,7 @@ def makecert(reqdir, subject, principal):
     Generate a certificate that can be used during unit testing.
     """
 
-    ra = rabase.rabase()
+    ra = rabase.rabase(api)
     if (not os.path.exists(ra.sec_dir) and
             api.env.xmlrpc_uri == 'http://localhost:8888/ipa/xml'):
         raise AssertionError('The self-signed CA is not configured, '
-- 
2.1.0

>From 5b817a7e1ceb7f5034539fcf0e1c0474bfc5238a Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 15 Jun 2015 12:06:34 +0000
Subject: [PATCH 07/13] plugable: Do not use DictProxy for API

https://fedorahosted.org/freeipa/ticket/3090
---
 ipalib/plugable.py | 59 +++++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 50 insertions(+), 9 deletions(-)

diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index bdd19c2..770897d 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -420,7 +420,7 @@ class Registrar(collections.Mapping):
         return len(self.__registry)
 
 
-class API(DictProxy):
+class API(ReadOnly):
     """
     Dynamic API object through which `Plugin` instances are accessed.
     """
@@ -428,12 +428,57 @@ class API(DictProxy):
     register = Registrar()
 
     def __init__(self, allowed, modules):
+        super(API, self).__init__()
         self.__plugins = {base: {} for base in allowed}
         self.modules = modules
-        self.__d = dict()
         self.__done = set()
         self.env = Env()
-        super(API, self).__init__(self.__d)
+        if not is_production_mode(self):
+            lock(self)
+
+    def __len__(self):
+        """
+        Return the number of plugin namespaces in this API object.
+        """
+        return len(self.__plugins)
+
+    def __iter__(self):
+        """
+        Iterate (in ascending order) through plugin namespace names.
+        """
+        return (base.__name__ for base in self.__plugins)
+
+    def __contains__(self, name):
+        """
+        Return True if this API object contains plugin namespace ``name``.
+
+        :param name: The plugin namespace name to test for membership.
+        """
+        return name in set(self)
+
+    def __getitem__(self, name):
+        """
+        Return the plugin namespace corresponding to ``name``.
+
+        :param name: The name of the plugin namespace you wish to retrieve.
+        """
+        if name in self:
+            try:
+                return getattr(self, name)
+            except AttributeError:
+                pass
+
+        raise KeyError(name)
+
+    def __call__(self):
+        """
+        Iterate (in ascending order by name) through plugin namespaces.
+        """
+        for name in self:
+            try:
+                yield getattr(self, name)
+            except AttributeError:
+                raise KeyError(name)
 
     def __doing(self, name):
         if name in self.__done:
@@ -749,13 +794,9 @@ class API(DictProxy):
                     '%s.%s' % (klass.__module__, klass.__name__),
                     []).append(name)
 
-            namespace = NameSpace(members)
             if not production_mode:
-                assert not (
-                    name in self.__d or hasattr(self, name)
-                )
-            self.__d[name] = namespace
-            object.__setattr__(self, name, namespace)
+                assert not hasattr(self, name)
+            object.__setattr__(self, name, NameSpace(members))
 
         for klass, instance in plugins.iteritems():
             if not production_mode:
-- 
2.1.0

>From 804831236ae83b5f0674c50075d287b74b5709d1 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 15 Jun 2015 11:50:15 +0000
Subject: [PATCH 08/13] plugable: Lock API on finalization rather than on
 initialization

https://fedorahosted.org/freeipa/ticket/3090
---
 ipalib/plugable.py | 19 +++++++++----------
 1 file changed, 9 insertions(+), 10 deletions(-)

diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index 770897d..5050416 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -433,8 +433,6 @@ class API(ReadOnly):
         self.modules = modules
         self.__done = set()
         self.env = Env()
-        if not is_production_mode(self):
-            lock(self)
 
     def __len__(self):
         """
@@ -501,14 +499,14 @@ class API(ReadOnly):
         self.__doing('bootstrap')
         self.env._bootstrap(**overrides)
         self.env._finalize_core(**dict(DEFAULT_CONFIG))
-        object.__setattr__(self, 'log_mgr', log_mgr)
+        self.log_mgr = log_mgr
         log = log_mgr.root_logger
-        object.__setattr__(self, 'log', log)
+        self.log = log
 
         # Add the argument parser
         if not parser:
             parser = self.build_global_parser()
-        object.__setattr__(self, 'parser', parser)
+        self.parser = parser
 
         # If logging has already been configured somewhere else (like in the
         # installer), don't add handlers or change levels:
@@ -796,7 +794,7 @@ class API(ReadOnly):
 
             if not production_mode:
                 assert not hasattr(self, name)
-            object.__setattr__(self, name, NameSpace(members))
+            setattr(self, name, NameSpace(members))
 
         for klass, instance in plugins.iteritems():
             if not production_mode:
@@ -806,10 +804,11 @@ class API(ReadOnly):
                 if not production_mode:
                     assert islocked(instance)
 
-        object.__setattr__(self, '_API__finalized', True)
-        object.__setattr__(self, 'plugins',
-            tuple((k, tuple(v)) for k, v in plugin_info.iteritems())
-        )
+        self.__finalized = True
+        self.plugins = tuple((k, tuple(v)) for k, v in plugin_info.iteritems())
+
+        if not production_mode:
+            lock(self)
 
 
 class IPAHelpFormatter(optparse.IndentedHelpFormatter):
-- 
2.1.0

>From ed7b26dc601840e2921c151d84e81b7c3c412dc1 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 22 Jun 2015 11:28:09 +0000
Subject: [PATCH 09/13] ipaplatform: Do not use MagicDict for KnownServices

https://fedorahosted.org/freeipa/ticket/3090
---
 ipaplatform/base/services.py | 25 +++++++++++++++++++++++--
 1 file changed, 23 insertions(+), 2 deletions(-)

diff --git a/ipaplatform/base/services.py b/ipaplatform/base/services.py
index 24d7a73..2e597f9 100644
--- a/ipaplatform/base/services.py
+++ b/ipaplatform/base/services.py
@@ -25,11 +25,11 @@ interacting with system services.
 
 import os
 import json
+import collections
 
 import ipalib
 from ipapython import ipautil
 from ipaplatform.paths import paths
-from ipalib.plugable import MagicDict
 
 # Canonical names of services as IPA wants to see them. As we need to have
 # *some* naming, set them as in Red Hat distributions. Actual implementation
@@ -54,7 +54,7 @@ wellknownports = {
 }
 
 
-class KnownServices(MagicDict):
+class KnownServices(collections.Mapping):
     """
     KnownServices is an abstract class factory that should give out instances
     of well-known platform services. Actual implementation must create these
@@ -62,6 +62,27 @@ class KnownServices(MagicDict):
     and cache them.
     """
 
+    def __init__(self, d):
+        self.__d = d
+
+    def __getitem__(self, key):
+        return self.__d[key]
+
+    def __iter__(self):
+        return iter(self.__d)
+
+    def __len__(self):
+        return len(self.__d)
+
+    def __call__(self):
+        return self.__d.itervalues()
+
+    def __getattr__(self, name):
+        try:
+            return self.__d[name]
+        except KeyError:
+            raise AttributeError(name)
+
 
 class PlatformService(object):
     """
-- 
2.1.0

>From efcabb877266a4a0e03e6138157596b13a632c8c Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 15 Jun 2015 12:06:47 +0000
Subject: [PATCH 10/13] plugable: Remove SetProxy, DictProxy and MagicDict

https://fedorahosted.org/freeipa/ticket/3090
---
 ipalib/plugable.py                    | 110 ----------------------
 ipatests/test_ipalib/test_plugable.py | 170 ----------------------------------
 2 files changed, 280 deletions(-)

diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index 5050416..9e7e4fc 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -105,116 +105,6 @@ class Registry(object):
         return decorator
 
 
-class SetProxy(ReadOnly):
-    """
-    A read-only container with set/sequence behaviour.
-
-    This container acts as a proxy to an actual set-like object (a set,
-    frozenset, or dict) that is passed to the constructor. To the extent
-    possible in Python, this underlying set-like object cannot be modified
-    through the SetProxy... which just means you wont do it accidentally.
-    """
-    def __init__(self, s):
-        """
-        :param s: The target set-like object (a set, frozenset, or dict)
-        """
-        allowed = (set, frozenset, dict)
-        if type(s) not in allowed:
-            raise TypeError('%r not in %r' % (type(s), allowed))
-        self.__s = s
-        if not is_production_mode(self):
-            lock(self)
-
-    def __len__(self):
-        """
-        Return the number of items in this container.
-        """
-        return len(self.__s)
-
-    def __iter__(self):
-        """
-        Iterate (in ascending order) through keys.
-        """
-        for key in sorted(self.__s):
-            yield key
-
-    def __contains__(self, key):
-        """
-        Return True if this container contains ``key``.
-
-        :param key: The key to test for membership.
-        """
-        return key in self.__s
-
-
-class DictProxy(SetProxy):
-    """
-    A read-only container with mapping behaviour.
-
-    This container acts as a proxy to an actual mapping object (a dict) that
-    is passed to the constructor. To the extent possible in Python, this
-    underlying mapping object cannot be modified through the DictProxy...
-    which just means you wont do it accidentally.
-
-    Also see `SetProxy`.
-    """
-    def __init__(self, d):
-        """
-        :param d: The target mapping object (a dict)
-        """
-        if type(d) is not dict:
-            raise TypeError('%r is not %r' % (type(d), dict))
-        self.__d = d
-        super(DictProxy, self).__init__(d)
-
-    def __getitem__(self, key):
-        """
-        Return the value corresponding to ``key``.
-
-        :param key: The key of the value you wish to retrieve.
-        """
-        return self.__d[key]
-
-    def __call__(self):
-        """
-        Iterate (in ascending order by key) through values.
-        """
-        for key in self:
-            yield self.__d[key]
-
-
-class MagicDict(DictProxy):
-    """
-    A mapping container whose values can be accessed as attributes.
-
-    For example:
-
-    >>> magic = MagicDict({'the_key': 'the value'})
-    >>> magic['the_key']
-    'the value'
-    >>> magic.the_key
-    'the value'
-
-    This container acts as a proxy to an actual mapping object (a dict) that
-    is passed to the constructor. To the extent possible in Python, this
-    underlying mapping object cannot be modified through the MagicDict...
-    which just means you wont do it accidentally.
-
-    Also see `DictProxy` and `SetProxy`.
-    """
-
-    def __getattr__(self, name):
-        """
-        Return the value corresponding to ``name``.
-
-        :param name: The name of the attribute you wish to retrieve.
-        """
-        try:
-            return self[name]
-        except KeyError:
-            raise AttributeError('no magic attribute %r' % name)
-
-
 class Plugin(ReadOnly):
     """
     Base class for all plugins.
diff --git a/ipatests/test_ipalib/test_plugable.py b/ipatests/test_ipalib/test_plugable.py
index 0d2763d..f5434e8 100644
--- a/ipatests/test_ipalib/test_plugable.py
+++ b/ipatests/test_ipalib/test_plugable.py
@@ -32,176 +32,6 @@ from ipalib import plugable, errors, text
 from ipaplatform.paths import paths
 
 
-class test_SetProxy(ClassChecker):
-    """
-    Test the `ipalib.plugable.SetProxy` class.
-    """
-    _cls = plugable.SetProxy
-
-    def test_class(self):
-        """
-        Test the `ipalib.plugable.SetProxy` class.
-        """
-        assert self.cls.__bases__ == (plugable.ReadOnly,)
-
-    def test_init(self):
-        """
-        Test the `ipalib.plugable.SetProxy.__init__` method.
-        """
-        okay = (set, frozenset, dict)
-        fail = (list, tuple)
-        for t in okay:
-            self.cls(t())
-            raises(TypeError, self.cls, t)
-        for t in fail:
-            raises(TypeError, self.cls, t())
-            raises(TypeError, self.cls, t)
-
-    def test_SetProxy(self):
-        """
-        Test container emulation of `ipalib.plugable.SetProxy` class.
-        """
-        def get_key(i):
-            return 'key_%d' % i
-
-        cnt = 10
-        target = set()
-        proxy = self.cls(target)
-        for i in xrange(cnt):
-            key = get_key(i)
-
-            # Check initial state
-            assert len(proxy) == len(target)
-            assert list(proxy) == sorted(target)
-            assert key not in proxy
-            assert key not in target
-
-            # Add and test again
-            target.add(key)
-            assert len(proxy) == len(target)
-            assert list(proxy) == sorted(target)
-            assert key in proxy
-            assert key in target
-
-
-class test_DictProxy(ClassChecker):
-    """
-    Test the `ipalib.plugable.DictProxy` class.
-    """
-    _cls = plugable.DictProxy
-
-    def test_class(self):
-        """
-        Test the `ipalib.plugable.DictProxy` class.
-        """
-        assert self.cls.__bases__ == (plugable.SetProxy,)
-
-    def test_init(self):
-        """
-        Test the `ipalib.plugable.DictProxy.__init__` method.
-        """
-        self.cls(dict())
-        raises(TypeError, self.cls, dict)
-        fail = (set, frozenset, list, tuple)
-        for t in fail:
-            raises(TypeError, self.cls, t())
-            raises(TypeError, self.cls, t)
-
-    def test_DictProxy(self):
-        """
-        Test container emulation of `ipalib.plugable.DictProxy` class.
-        """
-        def get_kv(i):
-            return (
-                'key_%d' % i,
-                'val_%d' % i,
-            )
-        cnt = 10
-        target = dict()
-        proxy = self.cls(target)
-        for i in xrange(cnt):
-            (key, val) = get_kv(i)
-
-            # Check initial state
-            assert len(proxy) == len(target)
-            assert list(proxy) == sorted(target)
-            assert list(proxy()) == [target[k] for k in sorted(target)]
-            assert key not in proxy
-            raises(KeyError, getitem, proxy, key)
-
-            # Add and test again
-            target[key] = val
-            assert len(proxy) == len(target)
-            assert list(proxy) == sorted(target)
-            assert list(proxy()) == [target[k] for k in sorted(target)]
-
-            # Verify TypeError is raised trying to set/del via proxy
-            raises(TypeError, setitem, proxy, key, val)
-            raises(TypeError, delitem, proxy, key)
-
-
-class test_MagicDict(ClassChecker):
-    """
-    Test the `ipalib.plugable.MagicDict` class.
-    """
-    _cls = plugable.MagicDict
-
-    def test_class(self):
-        """
-        Test the `ipalib.plugable.MagicDict` class.
-        """
-        assert self.cls.__bases__ == (plugable.DictProxy,)
-        for non_dict in ('hello', 69, object):
-            raises(TypeError, self.cls, non_dict)
-
-    def test_MagicDict(self):
-        """
-        Test container emulation of `ipalib.plugable.MagicDict` class.
-        """
-        cnt = 10
-        keys = []
-        d = dict()
-        dictproxy = self.cls(d)
-        for i in xrange(cnt):
-            key = 'key_%d' % i
-            val = 'val_%d' % i
-            keys.append(key)
-
-            # Test thet key does not yet exist
-            assert len(dictproxy) == i
-            assert key not in dictproxy
-            assert not hasattr(dictproxy, key)
-            raises(KeyError, getitem, dictproxy, key)
-            raises(AttributeError, getattr, dictproxy, key)
-
-            # Test that items/attributes cannot be set on dictproxy:
-            raises(TypeError, setitem, dictproxy, key, val)
-            raises(AttributeError, setattr, dictproxy, key, val)
-
-            # Test that additions in d are reflected in dictproxy:
-            d[key] = val
-            assert len(dictproxy) == i + 1
-            assert key in dictproxy
-            assert hasattr(dictproxy, key)
-            assert dictproxy[key] is val
-            assert read_only(dictproxy, key) is val
-
-        # Test __iter__
-        assert list(dictproxy) == keys
-
-        for key in keys:
-            # Test that items cannot be deleted through dictproxy:
-            raises(TypeError, delitem, dictproxy, key)
-            raises(AttributeError, delattr, dictproxy, key)
-
-            # Test that deletions in d are reflected in dictproxy
-            del d[key]
-            assert len(dictproxy) == len(d)
-            assert key not in dictproxy
-            raises(KeyError, getitem, dictproxy, key)
-            raises(AttributeError, getattr, dictproxy, key)
-
-
 class test_Plugin(ClassChecker):
     """
     Test the `ipalib.plugable.Plugin` class.
-- 
2.1.0

>From e4dc13bd80dc44e518349b07fd51222869ad8efd Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 22 Jun 2015 10:59:35 +0000
Subject: [PATCH 11/13] plugable: Change is_production_mode to method of API

https://fedorahosted.org/freeipa/ticket/3090
---
 ipalib/frontend.py                    |  4 +--
 ipalib/plugable.py                    | 22 +++++-------
 ipatests/test_ipalib/test_frontend.py | 66 +++++++++++++++++++++++++++++------
 ipatests/test_ipalib/test_plugable.py |  5 ++-
 4 files changed, 70 insertions(+), 27 deletions(-)

diff --git a/ipalib/frontend.py b/ipalib/frontend.py
index c36bfca..af201fc 100644
--- a/ipalib/frontend.py
+++ b/ipalib/frontend.py
@@ -27,7 +27,7 @@ from distutils import version
 from ipapython.version import API_VERSION
 from ipapython.ipa_log_manager import root_logger
 from base import NameSpace
-from plugable import Plugin, is_production_mode
+from plugable import Plugin
 from parameters import create_param, Param, Str, Flag, Password
 from output import Output, Entry, ListOfEntries
 from text import _
@@ -359,7 +359,7 @@ class HasParam(Plugin):
             self._filter_param_by_context(name, env),
             sort=False
         )
-        if not is_production_mode(self):
+        if not self.api.is_production_mode():
             check = getattr(self, 'check_' + name, None)
             if callable(check):
                 check(namespace)
diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index 9e7e4fc..967246f 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -49,17 +49,6 @@ from ipapython.version import VERSION, API_VERSION
 # FIXME: Updated constants.TYPE_ERROR to use this clearer format from wehjit:
 TYPE_ERROR = '%s: need a %r; got a %r: %r'
 
-def is_production_mode(obj):
-    """
-    If the object has self.env.mode defined and that mode is
-    production return True, otherwise return False.
-    """
-    if getattr(obj, 'env', None) is None:
-        return False
-    if getattr(obj.env, 'mode', None) is None:
-        return False
-    return obj.env.mode == 'production'
-
 
 # FIXME: This function has no unit test
 def find_modules_in_dir(src_dir):
@@ -184,7 +173,7 @@ class Plugin(ReadOnly):
             self.__finalize_called = True
             self._on_finalize()
             self.__finalized = True
-            if not is_production_mode(self):
+            if not self.__api.is_production_mode():
                 lock(self)
 
     def _on_finalize(self):
@@ -368,6 +357,13 @@ class API(ReadOnly):
             except AttributeError:
                 raise KeyError(name)
 
+    def is_production_mode(self):
+        """
+        If the object has self.env.mode defined and that mode is
+        production return True, otherwise return False.
+        """
+        return getattr(self.env, 'mode', None) == 'production'
+
     def __doing(self, name):
         if name in self.__done:
             raise StandardError(
@@ -664,7 +660,7 @@ class API(ReadOnly):
         self.__doing('finalize')
         self.__do_if_not_done('load_plugins')
 
-        production_mode = is_production_mode(self)
+        production_mode = self.is_production_mode()
         plugins = {}
         plugin_info = {}
 
diff --git a/ipatests/test_ipalib/test_frontend.py b/ipatests/test_ipalib/test_frontend.py
index 1e27dfe..3ca03c3 100644
--- a/ipatests/test_ipalib/test_frontend.py
+++ b/ipatests/test_ipalib/test_frontend.py
@@ -221,7 +221,10 @@ class test_Command(ClassChecker):
         """
         Helper method used to test args and options.
         """
-        api = 'the api instance'
+        class api(object):
+            @staticmethod
+            def is_production_mode():
+                return False
         class example(self.cls):
             takes_args = args
             takes_options = options
@@ -264,7 +267,10 @@ class test_Command(ClassChecker):
         """
         Test the ``ipalib.frontend.Command.args`` instance attribute.
         """
-        api = 'the api instance'
+        class api(object):
+            @staticmethod
+            def is_production_mode():
+                return False
         o = self.cls(api)
         o.finalize()
         assert type(o.args) is plugable.NameSpace
@@ -313,7 +319,10 @@ class test_Command(ClassChecker):
         """
         Test the ``ipalib.frontend.Command.options`` instance attribute.
         """
-        api = 'the api instance'
+        class api(object):
+            @staticmethod
+            def is_production_mode():
+                return False
         o = self.cls(api)
         o.finalize()
         assert type(o.options) is plugable.NameSpace
@@ -334,7 +343,10 @@ class test_Command(ClassChecker):
         """
         Test the ``ipalib.frontend.Command.output`` instance attribute.
         """
-        api = 'the api instance'
+        class api(object):
+            @staticmethod
+            def is_production_mode():
+                return False
         inst = self.cls(api)
         inst.finalize()
         assert type(inst.output) is plugable.NameSpace
@@ -381,6 +393,9 @@ class test_Command(ClassChecker):
         """
         class api(object):
             env = config.Env(context='cli')
+            @staticmethod
+            def is_production_mode():
+                return False
         class user_add(frontend.Command):
             takes_args = parameters.Str('uid',
                 normalizer=lambda value: value.lower(),
@@ -405,7 +420,10 @@ class test_Command(ClassChecker):
         """
         Test the `ipalib.frontend.Command.convert` method.
         """
-        api = 'the api instance'
+        class api(object):
+            @staticmethod
+            def is_production_mode():
+                return False
         kw = dict(
             option0=u'1.5',
             option1=u'7',
@@ -419,7 +437,10 @@ class test_Command(ClassChecker):
         """
         Test the `ipalib.frontend.Command.normalize` method.
         """
-        api = 'the api instance'
+        class api(object):
+            @staticmethod
+            def is_production_mode():
+                return False
         kw = dict(
             option0=u'OPTION0',
             option1=u'OPTION1',
@@ -467,6 +488,9 @@ class test_Command(ClassChecker):
         """
         class api(object):
             env = config.Env(context='cli')
+            @staticmethod
+            def is_production_mode():
+                return False
 
         sub = self.subcls(api)
         sub.finalize()
@@ -672,7 +696,10 @@ class test_Command(ClassChecker):
         """
         Test the `ipalib.frontend.Command.validate_output` method.
         """
-        api = 'the api instance'
+        class api(object):
+            @staticmethod
+            def is_production_mode():
+                return False
         class Example(self.cls):
             has_output = ('foo', 'bar', 'baz')
 
@@ -711,7 +738,10 @@ class test_Command(ClassChecker):
         """
         Test `ipalib.frontend.Command.validate_output` per-type validation.
         """
-        api = 'the api instance'
+        class api(object):
+            @staticmethod
+            def is_production_mode():
+                return False
 
         class Complex(self.cls):
             has_output = (
@@ -737,7 +767,10 @@ class test_Command(ClassChecker):
         """
         Test `ipalib.frontend.Command.validate_output` nested validation.
         """
-        api = 'the api instance'
+        class api(object):
+            @staticmethod
+            def is_production_mode():
+                return False
 
         class Subclass(output.ListOfEntries):
             pass
@@ -769,7 +802,10 @@ class test_Command(ClassChecker):
         """
         Test the `ipalib.frontend.Command.get_output_params` method.
         """
-        api = 'the api instance'
+        class api(object):
+            @staticmethod
+            def is_production_mode():
+                return False
         class example(self.cls):
             has_output_params = (
                 'one',
@@ -802,7 +838,10 @@ class test_LocalOrRemote(ClassChecker):
         """
         Test the `ipalib.frontend.LocalOrRemote.__init__` method.
         """
-        api = 'the api instance'
+        class api(object):
+            @staticmethod
+            def is_production_mode():
+                return False
         o = self.cls(api)
         o.finalize()
         assert list(o.args) == []
@@ -915,6 +954,8 @@ class test_Object(ClassChecker):
                 return hasattr(self, key)
             def __getitem__(self, key):
                 return getattr(self, key)
+            def is_production_mode(self):
+                return False
         api = FakeAPI()
         assert len(api.Method) == cnt * 3
 
@@ -1073,6 +1114,9 @@ class test_Attribute(ClassChecker):
         user_obj = 'The user frontend.Object instance'
         class api(object):
             Object = dict(user=user_obj)
+            @staticmethod
+            def is_production_mode():
+                return False
         class user_add(self.cls):
             pass
         o = user_add(api)
diff --git a/ipatests/test_ipalib/test_plugable.py b/ipatests/test_ipalib/test_plugable.py
index f5434e8..99554d4 100644
--- a/ipatests/test_ipalib/test_plugable.py
+++ b/ipatests/test_ipalib/test_plugable.py
@@ -88,7 +88,10 @@ class test_Plugin(ClassChecker):
         """
         Test the `ipalib.plugable.Plugin.finalize` method.
         """
-        api = 'the api instance'
+        class api(object):
+            @staticmethod
+            def is_production_mode():
+                return False
         o = self.cls(api)
         assert not o.__islocked__()
         o.finalize()
-- 
2.1.0

>From 466351ee71df73343fd670cef979c155e85607af Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 15 Jun 2015 13:36:26 +0000
Subject: [PATCH 12/13] plugable: Specify plugin base classes and modules using
 API properties

https://fedorahosted.org/freeipa/ticket/3090
---
 ipalib/__init__.py                    | 16 ++++++++--------
 ipalib/plugable.py                    | 25 +++++++++++++++++--------
 ipaserver/advise/base.py              |  7 ++++++-
 ipatests/test_ipalib/test_plugable.py |  6 +++++-
 4 files changed, 36 insertions(+), 18 deletions(-)

diff --git a/ipalib/__init__.py b/ipalib/__init__.py
index 4a66e7d..44aacd0 100644
--- a/ipalib/__init__.py
+++ b/ipalib/__init__.py
@@ -899,16 +899,16 @@ else:
 
 
 class API(plugable.API):
-    def __init__(self, allowed):
-        super(API, self).__init__(allowed, ['ipalib.plugins.*'])
-
-    def bootstrap(self, parser=None, **overrides):
-        super(API, self).bootstrap(parser, **overrides)
+    bases = (Command, Object, Method, Backend, Updater)
 
+    @property
+    def modules(self):
+        result = ('ipalib.plugins.*',)
         if self.env.in_server:
-            self.modules.append('ipaserver.plugins.*')
+            result += ('ipaserver.plugins.*',)
         if self.env.context in ('installer', 'updates'):
-            self.modules.append('ipaserver.install.plugins.*')
+            result += ('ipaserver.install.plugins.*',)
+        return result
 
 
 def create_api(mode='dummy'):
@@ -926,7 +926,7 @@ def create_api(mode='dummy'):
 
         - `backend.Backend`
     """
-    api = API((Command, Object, Method, Backend, Updater))
+    api = API()
     if mode is not None:
         api.env.mode = mode
     assert mode != 'production'
diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index 967246f..33b7a58 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -306,24 +306,31 @@ class API(ReadOnly):
 
     register = Registrar()
 
-    def __init__(self, allowed, modules):
+    def __init__(self):
         super(API, self).__init__()
-        self.__plugins = {base: {} for base in allowed}
-        self.modules = modules
+        self.__plugins = {}
         self.__done = set()
         self.env = Env()
 
+    @property
+    def bases(self):
+        raise NotImplementedError
+
+    @property
+    def modules(self):
+        raise NotImplementedError
+
     def __len__(self):
         """
         Return the number of plugin namespaces in this API object.
         """
-        return len(self.__plugins)
+        return len(self.bases)
 
     def __iter__(self):
         """
         Iterate (in ascending order) through plugin namespace names.
         """
-        return (base.__name__ for base in self.__plugins)
+        return (base.__name__ for base in self.bases)
 
     def __contains__(self, name):
         """
@@ -614,10 +621,11 @@ class API(ReadOnly):
 
         # Find the base class or raise SubclassError:
         found = False
-        for (base, sub_d) in self.__plugins.iteritems():
+        for base in self.bases:
             if not issubclass(klass, base):
                 continue
 
+            sub_d = self.__plugins.setdefault(base, {})
             found = True
 
             if sub_d.get(klass.__name__) is klass:
@@ -647,7 +655,7 @@ class API(ReadOnly):
         if not found:
             raise errors.PluginSubclassError(
                 plugin=klass,
-                bases=self.__plugins.keys(),
+                bases=self.bases,
             )
 
     def finalize(self):
@@ -664,8 +672,9 @@ class API(ReadOnly):
         plugins = {}
         plugin_info = {}
 
-        for base, sub_d in self.__plugins.iteritems():
+        for base in self.bases:
             name = base.__name__
+            sub_d = self.__plugins.get(base, {})
 
             members = []
             for klass in sub_d.itervalues():
diff --git a/ipaserver/advise/base.py b/ipaserver/advise/base.py
index e9873ac..f6c82b5 100644
--- a/ipaserver/advise/base.py
+++ b/ipaserver/advise/base.py
@@ -121,7 +121,12 @@ class Advice(Plugin):
 
         raise NotImplementedError
 
-advise_api = API((Advice,), ('ipaserver.advise.plugins.*',))
+
+class AdviseAPI(API):
+    bases = (Advice,)
+    modules = ('ipaserver.advise.plugins.*',)
+
+advise_api = AdviseAPI()
 
 
 class IpaAdvise(admintool.AdminTool):
diff --git a/ipatests/test_ipalib/test_plugable.py b/ipatests/test_ipalib/test_plugable.py
index 99554d4..eb16cce 100644
--- a/ipatests/test_ipalib/test_plugable.py
+++ b/ipatests/test_ipalib/test_plugable.py
@@ -202,7 +202,11 @@ class test_API(ClassChecker):
             def method(self, n):
                 return n + 1
 
-        api = plugable.API([base0, base1], [])
+        class API(plugable.API):
+            bases = (base0, base1)
+            modules = ()
+
+        api = API()
         api.env.mode = 'unit_test'
         api.env.in_tree = True
         r = api.add_plugin
-- 
2.1.0

>From 23098cab2995fbe42648800373109bbbc5673717 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 8 Jun 2015 12:35:29 +0000
Subject: [PATCH 13/13] plugable: Remove unused call method of Plugin

https://fedorahosted.org/freeipa/ticket/3090
---
 ipalib/plugable.py                    | 18 ------------------
 ipatests/test_ipalib/test_plugable.py | 10 ----------
 2 files changed, 28 deletions(-)

diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index 33b7a58..2ce7acf 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -31,7 +31,6 @@ import inspect
 import threading
 import os
 from os import path
-import subprocess
 import optparse
 import errors
 import textwrap
@@ -229,23 +228,6 @@ class Plugin(ReadOnly):
                     "attribute '%s' of plugin '%s' was not set in finalize()" % (self.name, obj.name)
                 )
 
-    def call(self, executable, *args):
-        """
-        Call ``executable`` with ``args`` using subprocess.call().
-
-        If the call exits with a non-zero exit status,
-        `ipalib.errors.SubprocessError` is raised, from which you can retrieve
-        the exit code by checking the SubprocessError.returncode attribute.
-
-        This method does *not* return what ``executable`` sent to stdout... for
-        that, use `Plugin.callread()`.
-        """
-        argv = (executable,) + args
-        self.debug('Calling %r', argv)
-        code = subprocess.call(argv)
-        if code != 0:
-            raise errors.SubprocessError(returncode=code, argv=argv)
-
     def __repr__(self):
         """
         Return 'module_name.class_name()' representation.
diff --git a/ipatests/test_ipalib/test_plugable.py b/ipatests/test_ipalib/test_plugable.py
index eb16cce..c0b88d1 100644
--- a/ipatests/test_ipalib/test_plugable.py
+++ b/ipatests/test_ipalib/test_plugable.py
@@ -97,16 +97,6 @@ class test_Plugin(ClassChecker):
         o.finalize()
         assert o.__islocked__()
 
-    def test_call(self):
-        """
-        Test the `ipalib.plugable.Plugin.call` method.
-        """
-        o = self.cls()
-        o.call(paths.BIN_TRUE) is None
-        e = raises(errors.SubprocessError, o.call, paths.BIN_FALSE)
-        assert e.returncode == 1
-        assert e.argv == (paths.BIN_FALSE,)
-
 
 def test_Registrar():
     """
-- 
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