>From a4e3c569d35e6f812ff641f38e89982d0254eaba Mon Sep 17 00:00:00 2001
From: Nikolai Kondrashov <Nikolai.Kondrashov@redhat.com>
Date: Mon, 24 Nov 2014 19:13:16 +0200
Subject: [PATCH 1/1] Add integration tests

Add --enable-intgcheck configure option, which enables "intgcheck" make
target. CI is updated to use them.

The "intgcheck" target configures and builds sssd in a sub-directory,
installs it into a prefix in another sub-directory, and then makes the
"intgcheck-installed" target from within src/tests/intg in that separate
build.

The "intgcheck-installed" target in src/tests/intg runs py.test for all
tests it can find in that directory, under fakeroot and
nss_wrapper/uid_wrapper environments emulating running under root.

The only test in src/tests/intg at the moment - ldap_test.py sets up and
starts a slapd instance, adds a few user and group entries, configures
and starts sssd and verifies that those users and groups are retrieved
correctly using various nss functions.

The above only works on Red Hat distros, mostly because Debian lacks
nss_wrapper package. The tests are very basic and error reporting on
mismatch in data received from nss is very rudimentary, at the moment.
There is also a good amount of duplication between rfc2307 and
rfc2307bis cases, which might be dealt with later.
---
 Makefile.am                   |  29 ++++-
 configure.ac                  |  15 ++-
 contrib/ci/configure.sh       |   6 +
 contrib/ci/deps.sh            |  11 +-
 contrib/ci/run                |   9 ++
 src/external/fakeroot.m4      |   2 +
 src/external/intgcheck.m4     |  23 ++++
 src/external/ldap.m4          |   6 +
 src/external/pytest.m4        |   3 +
 src/external/python.m4        |  49 +++++++
 src/tests/intg/Makefile.am    |  59 +++++++++
 src/tests/intg/config.py.m4   |  13 ++
 src/tests/intg/ent.py         | 163 +++++++++++++++++++++++
 src/tests/intg/ldap_test.py   | 296 ++++++++++++++++++++++++++++++++++++++++++
 src/tests/intg/misc.py        |  49 +++++++
 src/tests/intg/slapd_setup    | 246 +++++++++++++++++++++++++++++++++++
 src/tests/intg/slapd_teardown |  49 +++++++
 17 files changed, 1024 insertions(+), 4 deletions(-)
 create mode 100644 src/external/fakeroot.m4
 create mode 100644 src/external/intgcheck.m4
 create mode 100644 src/external/pytest.m4
 create mode 100644 src/tests/intg/Makefile.am
 create mode 100644 src/tests/intg/config.py.m4
 create mode 100644 src/tests/intg/ent.py
 create mode 100644 src/tests/intg/ldap_test.py
 create mode 100644 src/tests/intg/misc.py
 create mode 100755 src/tests/intg/slapd_setup
 create mode 100755 src/tests/intg/slapd_teardown

diff --git a/Makefile.am b/Makefile.am
index 8f0ce4b..647f1e8 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -19,7 +19,7 @@ if HAVE_MANPAGES
 SUBDIRS += src/man
 endif
 
-SUBDIRS += . src/tests/cwrap
+SUBDIRS += . src/tests/cwrap src/tests/intg
 
 # Some old versions of automake don't define builddir
 builddir ?= .
@@ -2280,6 +2280,33 @@ autofs_test_client_CFLAGS = $(AM_CFLAGS)
 autofs_test_client_LDADD = -lpopt $(CLIENT_LIBS)
 endif
 
+#####################
+# Integration tests #
+#####################
+
+if ENABLE_INTGCHECK
+intgcheck:
+	set -e; \
+	rm -Rf intg; \
+	$(MKDIR_P) intg/bld; \
+	: Use /hopefully/ short prefix to keep D-Bus socket path short; \
+	prefix=`mktemp --tmpdir --directory sssd-intg.XXXXXXXX`; \
+	$(LN_S) "$$prefix" intg/pfx; \
+	cd intg/bld; \
+	$(abs_top_srcdir)/configure \
+	    --prefix="$$prefix" \
+	    --with-ldb-lib-dir="$$prefix"/lib/ldb \
+	    $(INTGCHECK_CONFIGURE_FLAGS); \
+	$(MAKE) $(AM_MAKEFLAGS); \
+	: Force single-thread install to workaround concurrency issues; \
+	$(MAKE) $(AM_MAKEFLAGS) -j1 install; \
+	: Remove .la files from LDB module directory to avoid loader warnings; \
+	rm "$$prefix"/lib/ldb/*.la; \
+	$(MAKE) $(AM_MAKEFLAGS) -C src/tests/intg intgcheck-installed; \
+	cd ../..; \
+	rm -Rf "$$prefix" intg
+endif # ENABLE_INTGCHECK
+
 ####################
 # Client Libraries #
 ####################
diff --git a/configure.ac b/configure.ac
index e30405f..225ff64 100644
--- a/configure.ac
+++ b/configure.ac
@@ -174,6 +174,9 @@ m4_include([src/external/configlib.m4])
 m4_include([src/external/libnfsidmap.m4])
 m4_include([src/external/cwrap.m4])
 m4_include([src/external/libresolv.m4])
+m4_include([src/external/fakeroot.m4])
+m4_include([src/external/pytest.m4])
+m4_include([src/external/intgcheck.m4])
 
 if test x$build_config_lib = xyes; then
     m4_include([src/external/libaugeas.m4])
@@ -248,8 +251,8 @@ AM_CONDITIONAL([HAVE_PROFILE_CATALOGS], [test "x$HAVE_PROFILE_CATALOGS" != "x"])
 AM_CONDITIONAL([HAVE_MANPAGES], [test "x$HAVE_MANPAGES" != "x"])
 AM_CONDITIONAL([HAVE_PO4A], [test "x$PO4A" != "xno"])
 
+AC_PATH_PROG(PYTHON2, python2)
 if test x$HAVE_PYTHON2_BINDINGS = x1; then
-    AC_PATH_PROG(PYTHON2, python2)
     PYTHON=$PYTHON2
     AM_PATH_PYTHON([2.6])
     AM_PYTHON_CONFIG([python2])
@@ -268,9 +271,10 @@ if test x$HAVE_PYTHON2_BINDINGS = x1; then
 
     SSS_CLEAN_PYTHON_VARIABLES
 fi
+AM_CONDITIONAL([HAVE_PYTHON2], [test x"$PYTHON2" != x])
 
+AC_PATH_PROG(PYTHON3, python3)
 if test x$HAVE_PYTHON3_BINDINGS = x1; then
-    AC_PATH_PROG(PYTHON3, python3)
     PYTHON=$PYTHON3
     AM_PATH_PYTHON([3.3])
     AM_PYTHON_CONFIG([python3])
@@ -289,11 +293,15 @@ if test x$HAVE_PYTHON3_BINDINGS = x1; then
 
     SSS_CLEAN_PYTHON_VARIABLES
 fi
+AM_CONDITIONAL([HAVE_PYTHON3], [test x"$PYTHON3" != x])
 
 AM_CONDITIONAL([BUILD_PYTHON_BINDINGS],
                [test x"$with_python2_bindings" = xyes \
                      -o x"$with_python3_bindings" = xyes])
 
+AM_PYTHON2_MODULE([ldap])
+AM_CONDITIONAL([HAVE_PY2MOD_LDAP], [test x"$HAVE_PY2MOD_LDAP" == xyes])
+
 if test x$HAVE_SELINUX != x; then
     AM_CHECK_SELINUX
     AM_CHECK_SELINUX_LOGIN_DIR
@@ -366,6 +374,8 @@ AM_CHECK_CMOCKA
 AM_CHECK_UID_WRAPPER
 AM_CHECK_NSS_WRAPPER
 
+AM_ENABLE_INTGCHECK
+
 AM_CONDITIONAL([HAVE_DEVSHM], [test -d /dev/shm])
 
 abs_build_dir=`pwd`
@@ -375,6 +385,7 @@ AC_SUBST([abs_builddir], $abs_build_dir)
 AC_CONFIG_FILES([Makefile contrib/sssd.spec src/examples/rwtab src/doxy.config
                  src/sysv/sssd src/sysv/gentoo/sssd src/sysv/SUSE/sssd
                  po/Makefile.in src/man/Makefile src/tests/cwrap/Makefile
+                 src/tests/intg/Makefile
                  src/providers/ipa/ipa_hbac.pc src/providers/ipa/ipa_hbac.doxy
                  src/lib/idmap/sss_idmap.pc src/lib/idmap/sss_idmap.doxy
                  src/sss_client/sudo/sss_sudo.doxy
diff --git a/contrib/ci/configure.sh b/contrib/ci/configure.sh
index d5d4c79..980b5dc 100644
--- a/contrib/ci/configure.sh
+++ b/contrib/ci/configure.sh
@@ -20,6 +20,7 @@ if [ -z ${_CONFIGURE_SH+set} ]; then
 declare -r _CONFIGURE_SH=
 
 . distro.sh
+. deps.sh
 
 # List of "configure" arguments.
 declare -a CONFIGURE_ARG_LIST=(
@@ -44,6 +45,11 @@ if [[ "$DISTRO_BRANCH" == -redhat-redhatenterprise*-7.*- ]]; then
         "--without-python3-bindings"
     )
 fi
+
+if "$DEPS_INTGCHECK_SATISFIED"; then
+    CONFIGURE_ARG_LIST+=("--enable-intgcheck")
+fi
+
 declare -r -a CONFIGURE_ARG_LIST
 
 fi # _CONFIGURE_SH
diff --git a/contrib/ci/deps.sh b/contrib/ci/deps.sh
index 4e0ce1e..8f87203 100644
--- a/contrib/ci/deps.sh
+++ b/contrib/ci/deps.sh
@@ -27,15 +27,23 @@ declare -a DEPS_LIST=(
     valgrind
 )
 
+# "Integration tests dependencies satisfied" flag
+declare DEPS_INTGCHECK_SATISFIED=true
+
 if [[ "$DISTRO_BRANCH" == -redhat-* ]]; then
     declare _DEPS_LIST_SPEC
     DEPS_LIST+=(
         clang-analyzer
+        fakeroot
         libcmocka-devel
         mock
+        nss_wrapper
+        openldap-clients
+        openldap-servers
+        pytest
+        python-ldap
         rpm-build
         uid_wrapper
-        nss_wrapper
     )
     _DEPS_LIST_SPEC=`
         sed -e 's/@PACKAGE_VERSION@/0/g' \
@@ -98,6 +106,7 @@ if [[ "$DISTRO_BRANCH" == -debian-* ]]; then
         xml-core
         xsltproc
     )
+    DEPS_INTGCHECK_SATISFIED=false
 fi
 
 declare -a -r DEPS_LIST
diff --git a/contrib/ci/run b/contrib/ci/run
index 2f81a00..5f668ff 100755
--- a/contrib/ci/run
+++ b/contrib/ci/run
@@ -188,6 +188,7 @@ function build_debug()
     export CFLAGS="$DEBUG_CFLAGS"
     declare test_dir
     declare test_dir_distcheck
+    declare intgcheck_configure_args
     declare distcheck_configure_args
     declare status
 
@@ -217,6 +218,14 @@ function build_debug()
     ((status == 0))
 
     if "$MODERATE"; then
+        if "$DEPS_INTGCHECK_SATISFIED"; then
+            printf -v intgcheck_configure_args " %q" \
+                        "${CONFIGURE_ARG_LIST[@]}"
+            stage make-intgcheck make -j $CPU_NUM intgcheck \
+                                      INTGCHECK_CONFIGURE_FLAGS=" \
+                                        $intgcheck_configure_args"
+        fi
+
         test_dir_distcheck=`mktemp --directory /dev/shm/ci-test-dir.XXXXXXXX`
         # Single thread due to https://fedorahosted.org/sssd/ticket/2354
         status=0
diff --git a/src/external/fakeroot.m4 b/src/external/fakeroot.m4
new file mode 100644
index 0000000..685eaf8
--- /dev/null
+++ b/src/external/fakeroot.m4
@@ -0,0 +1,2 @@
+AC_CHECK_PROG([HAVE_FAKEROOT], [fakeroot], [true], [false])
+AM_CONDITIONAL([HAVE_FAKEROOT], [$HAVE_FAKEROOT])
diff --git a/src/external/intgcheck.m4 b/src/external/intgcheck.m4
new file mode 100644
index 0000000..1f919bd
--- /dev/null
+++ b/src/external/intgcheck.m4
@@ -0,0 +1,23 @@
+AC_DEFUN([AM_INTGCHECK_REQ], [
+    AM_COND_IF([$1], , [
+        AC_MSG_ERROR([cannot enable integration tests: $2 not found])
+    ])
+])
+AC_DEFUN([AM_ENABLE_INTGCHECK], [
+    AC_ARG_ENABLE(intgcheck,
+                  [AS_HELP_STRING([--enable-intgcheck],
+                                  [enable integration test support (intgcheck target) [default=no]])],
+                  enable_intgcheck="$enableval",
+                  enable_intgcheck="no")
+    if test x"$enable_intgcheck" == xyes; then
+        AM_INTGCHECK_REQ([HAVE_UID_WRAPPER],    [uid_wrapper])
+        AM_INTGCHECK_REQ([HAVE_NSS_WRAPPER],    [nss_wrapper])
+        AM_INTGCHECK_REQ([HAVE_SLAPD],          [slapd])
+        AM_INTGCHECK_REQ([HAVE_LDAPMODIFY],     [ldapmodify])
+        AM_INTGCHECK_REQ([HAVE_FAKEROOT],       [fakeroot])
+        AM_INTGCHECK_REQ([HAVE_PYTHON2],        [python2])
+        AM_INTGCHECK_REQ([HAVE_PYTEST],         [pytest])
+        AM_INTGCHECK_REQ([HAVE_PY2MOD_LDAP],    [python-ldap])
+    fi
+    AM_CONDITIONAL([ENABLE_INTGCHECK], [test x"$enable_intgcheck" == xyes])
+])
diff --git a/src/external/ldap.m4 b/src/external/ldap.m4
index 3a99ddf..be71f4c 100644
--- a/src/external/ldap.m4
+++ b/src/external/ldap.m4
@@ -90,3 +90,9 @@ AC_CHECK_TYPE([LDAPDerefRes],
 CFLAGS=$SAVE_CFLAGS
 LIBS=$SAVE_LIBS
 
+AC_PATH_PROG([SLAPD], [slapd], ,
+             [$PATH$PATH_SEPARATOR/usr/sbin$PATH_SEPARATOR])
+AC_SUBST([SLAPD])
+AM_CONDITIONAL([HAVE_SLAPD], [test x"$SLAPD" != x])
+AC_CHECK_PROG([HAVE_LDAPMODIFY], [ldapmodify], [true], [false])
+AM_CONDITIONAL([HAVE_LDAPMODIFY], [$HAVE_LDAPMODIFY])
diff --git a/src/external/pytest.m4 b/src/external/pytest.m4
new file mode 100644
index 0000000..00f28e5
--- /dev/null
+++ b/src/external/pytest.m4
@@ -0,0 +1,3 @@
+AC_PATH_PROG([PYTEST], [py.test])
+AC_SUBST([PYTEST])
+AM_CONDITIONAL([HAVE_PYTEST], [test x"$PYTEST" != x])
diff --git a/src/external/python.m4 b/src/external/python.m4
index c91e8df..691e600 100644
--- a/src/external/python.m4
+++ b/src/external/python.m4
@@ -68,3 +68,52 @@ AC_DEFUN([SSS_CLEAN_PYTHON_VARIABLES],
     unset am_cv_python_platform am_cv_python_pythondir am_cv_python_pyexecdir
     unset ac_cv_path_PYTHON_CONFIG
 ])
+
+dnl ===========================================================================
+dnl     http://www.gnu.org/software/autoconf-archive/ax_python_module.html
+dnl ===========================================================================
+dnl
+dnl SYNOPSIS
+dnl
+dnl   AM_PYTHON2_MODULE(modname[, fatal])
+dnl
+dnl DESCRIPTION
+dnl
+dnl   Checks for Python 2 module.
+dnl
+dnl   If fatal is non-empty then absence of a module will trigger an error.
+dnl
+dnl LICENSE
+dnl
+dnl   Copyright (c) 2008 Andrew Collier
+dnl
+dnl   Copying and distribution of this file, with or without modification, are
+dnl   permitted in any medium without royalty provided the copyright notice
+dnl   and this notice are preserved. This file is offered as-is, without any
+dnl   warranty.
+AC_DEFUN([AM_PYTHON2_MODULE],[
+    if test x"$PYTHON2" = x; then
+        if test -n "$2"; then
+            AC_MSG_ERROR([cannot look for $1 module: Python 2 not found])
+        else
+            AC_MSG_NOTICE([cannot look for $1 module: Python 2 not found])
+            eval AS_TR_CPP(HAVE_PY2MOD_$1)=no
+        fi
+    else
+        AC_MSG_CHECKING($(basename $PYTHON2) module: $1)
+        $PYTHON2 -c "import $1" 2>/dev/null
+        if test $? -eq 0; then
+            AC_MSG_RESULT(yes)
+            eval AS_TR_CPP(HAVE_PY2MOD_$1)=yes
+        else
+            AC_MSG_RESULT(no)
+            eval AS_TR_CPP(HAVE_PY2MOD_$1)=no
+            #
+            if test -n "$2"
+            then
+                AC_MSG_ERROR(failed to find required module $1)
+                exit 1
+            fi
+        fi
+    fi
+])
diff --git a/src/tests/intg/Makefile.am b/src/tests/intg/Makefile.am
new file mode 100644
index 0000000..dc1d6f0
--- /dev/null
+++ b/src/tests/intg/Makefile.am
@@ -0,0 +1,59 @@
+dist_noinst_SCRIPTS = \
+    slapd_setup \
+    slapd_teardown \
+    $(NULL)
+
+dist_noinst_DATA = \
+    config.py.m4 \
+    ent.py \
+    ldap_test.py \
+    misc.py \
+    $(NULL)
+
+config.py: config.py.m4
+	m4 -D "prefix=\`$(prefix)'" \
+	   -D "sysconfdir=\`$(sysconfdir)'" \
+	   -D "dbpath=\`$(dbpath)'" \
+	   -D "pidpath=\`$(pidpath)'" \
+	   -D "logpath=\`$(logpath)'" \
+	   -D "mcpath=\`$(mcpath)'" \
+	   $< > $@
+
+root:
+	$(MKDIR_P) -m 0700 root/.dbus-keyrings
+
+passwd: root
+	echo "root:x:0:0:root:$(abs_builddir)/root:/bin/bash" > $@
+
+group:
+	echo "root:x:0:" > $@
+
+CLEANFILES=config.py config.pyc passwd group
+
+clean-local:
+	rm -Rf root
+
+intgcheck-installed: config.py passwd group
+	pipepath="$(DESTDIR)$(pipepath)"; \
+	if test $${#pipepath} -gt 80; then \
+	    echo "error: Pipe directory path too long," \
+	         "D-Bus won't be able to open sockets" >&2; \
+	    exit 1; \
+	fi
+	set -e; \
+	nss_wrapper=$$(pkg-config --libs nss_wrapper); \
+	uid_wrapper=$$(pkg-config --libs uid_wrapper); \
+	PATH="$$(dirname -- $(SLAPD)):$$PATH" \
+	PATH="$(DESTDIR)$(sbindir):$(DESTDIR)$(bindir):$$PATH" \
+	PATH="$(abs_builddir):$(abs_srcdir):$$PATH" \
+	PYTHONPATH="$(abs_builddir):$(abs_srcdir)" \
+	LDB_MODULES_PATH="$(DESTDIR)$(ldblibdir)" \
+	LD_PRELOAD="$$nss_wrapper $$uid_wrapper" \
+	NSS_WRAPPER_PASSWD="$(abs_builddir)/passwd" \
+	NSS_WRAPPER_GROUP="$(abs_builddir)/group" \
+	NSS_WRAPPER_MODULE_SO_PATH="$(DESTDIR)$(nsslibdir)/libnss_sss.so.2" \
+	NSS_WRAPPER_MODULE_FN_PREFIX="sss" \
+	UID_WRAPPER=1 \
+	UID_WRAPPER_ROOT=1 \
+	    fakeroot $(PYTHON2) $(PYTEST) --tb=native "$(abs_srcdir)"
+	rm -f $(DESTDIR)$(logpath)/*
diff --git a/src/tests/intg/config.py.m4 b/src/tests/intg/config.py.m4
new file mode 100644
index 0000000..563127c
--- /dev/null
+++ b/src/tests/intg/config.py.m4
@@ -0,0 +1,13 @@
+"""
+Build configuration variables.
+"""
+
+PREFIX              = "prefix"
+SYSCONFDIR          = "sysconfdir"
+SSSDCONFDIR         = SYSCONFDIR + "/sssd"
+CONF_PATH           = SSSDCONFDIR + "/sssd.conf"
+DB_PATH             = "dbpath"
+PID_PATH            = "pidpath"
+PIDFILE_PATH        = PID_PATH + "/sssd.pid"
+LOG_PATH            = "logpath"
+MCACHE_PATH         = "mcpath"
diff --git a/src/tests/intg/ent.py b/src/tests/intg/ent.py
new file mode 100644
index 0000000..71ac79d
--- /dev/null
+++ b/src/tests/intg/ent.py
@@ -0,0 +1,163 @@
+#
+# Abstract passwd/group entry dictionary
+#
+# Copyright (c) 2015 Red Hat, Inc.
+# Author: Nikolai Kondrashov <Nikolai.Kondrashov@redhat.com>
+#
+# This is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+#
+# An abstract entry is a dictionary storing passwd or group entries with
+# structure field names as keys, but with pw_/gr_ prefixes removed. These
+# allow iterating over fields as opposed to entries returned by pwd and grp
+# modules.
+#
+
+import pwd
+import grp
+
+def from_passwd(passwd):
+    """Convert a passwd entry returned by pwd module to an entry dictionary"""
+    return dict(
+            name    = passwd.pw_name,
+            passwd  = passwd.pw_passwd,
+            uid     = passwd.pw_uid,
+            gid     = passwd.pw_gid,
+            gecos   = passwd.pw_gecos,
+            dir     = passwd.pw_dir,
+            shell   = passwd.pw_shell
+    )
+
+def from_group(group):
+    """Convert a group entry returned by grp module to an entry dictionary"""
+    return dict(
+            name    = group.gr_name,
+            passwd  = group.gr_passwd,
+            gid     = group.gr_gid,
+            mem     = group.gr_mem
+    )
+
+def match(ent, pattern):
+    """Match a user/group (sub-)entry against a pattern"""
+    if isinstance(pattern, dict):
+        if not isinstance(ent, dict):
+            return False
+        for key, value in pattern.iteritems():
+            if not match(ent[key], value):
+                return False
+    elif isinstance(pattern, list):
+        if not isinstance(ent, list) or not list_match(ent, pattern):
+            return False
+    else:
+        if pattern != ent:
+            return False
+    return True
+
+def list_match(ent_list, pattern_list):
+    """
+        Match a user/group (sub-)entry list against a pattern list
+    """
+    matched_pattern_num = 0
+    left_ent_list = ent_list[:]
+    for pi, pv in enumerate(pattern_list):
+        for ei, ev in enumerate(left_ent_list):
+            if match(ev, pv):
+                matched_pattern_num += 1
+                del left_ent_list[ei]
+                break
+    return matched_pattern_num == len(pattern_list) and \
+           len(left_ent_list) == 0
+
+def passwd_list_remove_root(passwd_list):
+    """
+        Remove root user from a passwd list produced by pwd.getpwall()
+    """
+    for i, v in enumerate(passwd_list):
+        if v.pw_name == "root" and v.pw_uid == 0 and v.pw_gid == 0:
+            del passwd_list[i]
+            return passwd_list
+    raise Exception("no root user found")
+
+def group_list_remove_root(group_list):
+    """
+        Remove root group from a group list produced by grp.getgrall()
+    """
+    for i, v in enumerate(group_list):
+        if v.gr_name == "root" and v.gr_gid == 0:
+            del group_list[i]
+            return group_list
+    raise Exception("no root group found")
+
+def passwd_get_by_name(name):
+    """Get a passwd database entry by name"""
+    return from_passwd(pwd.getpwnam(name))
+
+def passwd_get_by_uid(uid):
+    """Get a passwd database entry by UID"""
+    return from_passwd(pwd.getpwuid(uid))
+
+def group_get_by_name(name):
+    """Get a group database entry by name"""
+    return from_group(grp.getgrnam(name))
+
+def group_get_by_gid(gid):
+    """Get a group database entry by GID"""
+    return from_group(grp.getgrgid(gid))
+
+def passwd_list_get():
+    """Get passwd database entry dictionary list"""
+    return map(from_passwd, passwd_list_remove_root(pwd.getpwall()))
+
+def group_list_get():
+    """Get group database entry dictionary list"""
+    return map(from_group, group_list_remove_root(grp.getgrall()))
+
+def passwd_list_match(pattern_list, enum):
+    """Match passwd database against a dictionary pattern list"""
+    if enum and not list_match(passwd_list_get(), pattern_list):
+        return False
+    for pattern in pattern_list:
+        if "name" in pattern:
+            try:
+                if not match(passwd_get_by_name(pattern["name"]), pattern):
+                    return False
+            except KeyError:
+                return False
+        if "uid" in pattern:
+            try:
+                if not match(passwd_get_by_uid(pattern["uid"]), pattern):
+                    return False
+            except KeyError:
+                return False
+    return True
+
+def group_list_match(pattern_list, enum):
+    """Match group database against a dictionary pattern list"""
+    if enum and not list_match(group_list_get(), pattern_list):
+        return False
+    for pattern in pattern_list:
+        if "name" in pattern:
+            try:
+                if not match(group_get_by_name(pattern["name"]), pattern):
+                    return False
+            except KeyError:
+                return False
+        if "uid" in pattern:
+            try:
+                if not match(group_get_by_gid(pattern["gid"]), pattern):
+                    return False
+            except KeyError:
+                return False
+    return True
+
diff --git a/src/tests/intg/ldap_test.py b/src/tests/intg/ldap_test.py
new file mode 100644
index 0000000..a8b8b6f
--- /dev/null
+++ b/src/tests/intg/ldap_test.py
@@ -0,0 +1,296 @@
+import os
+import stat
+import pwd
+import grp
+import ent
+import config
+import signal
+import subprocess
+import time
+import ldap
+import pytest
+from misc import *
+
+LDAP_URL="ldap://localhost:10389"
+LDAP_BASE_DN="dc=example,dc=com"
+LDAP_ADMIN_RDN="cn=admin"
+LDAP_ADMIN_DN=LDAP_ADMIN_RDN + "," + LDAP_BASE_DN
+LDAP_ADMIN_PW="Secret123"
+
+@pytest.fixture(scope="module")
+def ldap_inst(request):
+    """LDAP server instance fixture"""
+    def teardown():
+        if subprocess.call(["slapd_teardown", config.PREFIX]) != 0:
+            raise Exception("slapd teardown failed")
+    if subprocess.call(["slapd_setup", config.PREFIX, LDAP_URL,
+                        LDAP_BASE_DN, LDAP_ADMIN_RDN, LDAP_ADMIN_PW]) != 0:
+        teardown()
+        raise Exception("slapd setup failed")
+    request.addfinalizer(teardown)
+    return None
+
+@pytest.fixture(scope="module")
+def ldap_conn(request, ldap_inst):
+    """LDAP server connection fixture"""
+    ldap_conn = ldap.initialize(LDAP_URL)
+    ldap_conn.simple_bind_s(LDAP_ADMIN_DN, LDAP_ADMIN_PW)
+    def teardown():
+        ldap_conn.unbind_s()
+    request.addfinalizer(teardown)
+    return ldap_conn
+
+def ldap_fixture(request, ldap_conn, ent_list):
+    """Add LDAP entries and add teardown for removing them"""
+    for entry in ent_list:
+        ldap_conn.add_s(entry[0], entry[1])
+    def teardown():
+        for entry in ent_list:
+            ldap_conn.delete_s(entry[0])
+    request.addfinalizer(teardown)
+
+def conf_fixture(request, contents):
+    """Generate sssd.conf and add teardown for removing it"""
+    conf = open(config.CONF_PATH, "w")
+    conf.write(contents)
+    conf.close()
+    os.chmod(config.CONF_PATH, stat.S_IRUSR | stat.S_IWUSR)
+    request.addfinalizer(lambda: os.unlink(config.CONF_PATH))
+
+def sssd_fixture(request):
+    """Start sssd and add teardown for stopping it and removing state"""
+    if subprocess.call(["sssd", "-D", "-f"]) != 0:
+        raise Exception("sssd start failed")
+    def teardown():
+        try:
+            pid_file = open(config.PIDFILE_PATH, "r")
+            pid = int(pid_file.read())
+            os.kill(pid, signal.SIGTERM)
+            while True:
+                try:
+                    os.kill(pid, signal.SIGCONT)
+                except:
+                    break
+                time.sleep(1)
+        except:
+            pass
+        for path in os.listdir(config.DB_PATH):
+            os.unlink(config.DB_PATH + "/" + path)
+        for path in os.listdir(config.MCACHE_PATH):
+            os.unlink(config.MCACHE_PATH + "/" + path)
+    request.addfinalizer(teardown)
+
+def ldap_group(cn, gidNumber, member_uids=[]):
+    """
+        Generate a LDAP RFC2307 group data structure for passing to ldap.add_s
+    """
+    gidNumber = str(gidNumber)
+    attr_list = [
+        ('objectClass', ['top', 'posixGroup']),
+        ('gidNumber', [gidNumber])
+    ]
+    if len(member_uids) > 0:
+        attr_list.append(('memberUid', member_uids))
+    return ("cn=" + cn + ",ou=Groups," + LDAP_BASE_DN, attr_list)
+
+def ldap_group_bis(cn, gidNumber, member_uids=[], member_gids=[]):
+    """
+        Generate a LDAP RFC2307bis group data structure for passing to
+        ldap.add_s
+    """
+    gidNumber = str(gidNumber)
+    attr_list = [
+        ('objectClass', ['top', 'extensibleObject', 'groupOfNames']),
+        ('gidNumber', [gidNumber])
+    ]
+    if len(member_uids) > 0:
+        attr_list.append(
+            ('member', [
+                "uid=" + uid + ",ou=Users," + LDAP_BASE_DN for
+                    uid in member_uids
+            ])
+        )
+    if len(member_gids) > 0:
+        attr_list.append(
+            ('member', [
+                "cn=" + gid + ",ou=Groups," + LDAP_BASE_DN for
+                    gid in member_gids
+            ])
+        )
+    return ("cn=" + cn + ",ou=Groups," + LDAP_BASE_DN, attr_list)
+
+def ldap_user(uid, uidNumber, gidNumber):
+    """Generate a LDAP user data structure for passing to ldap.add_s"""
+    uidNumber = str(uidNumber)
+    gidNumber = str(gidNumber)
+    return (
+        "uid=" + uid + ",ou=Users," + LDAP_BASE_DN,
+        [
+            ('objectClass',     ['top', 'inetOrgPerson', 'posixAccount']),
+            ('cn',              [uidNumber]),
+            ('sn',              ['User']),
+            ('uidNumber',       [uidNumber]),
+            ('gidNumber',       [gidNumber]),
+            ('userPassword',    ['Password' + uidNumber]),
+            ('homeDirectory',   ['/home/' + uid]),
+            ('loginShell',      ['/bin/bash']),
+        ]
+    )
+
+@pytest.fixture
+def sanity_rfc2307(request, ldap_conn):
+    ldap_fixture(request, ldap_conn,
+        [
+            ldap_user("user1", 1001, 2001),
+            ldap_user("user2", 1002, 2002),
+            ldap_user("user3", 1003, 2003),
+
+            ldap_group("group1", 2001),
+            ldap_group("group2", 2002),
+            ldap_group("group3", 2003),
+
+            ldap_group("empty_group", 2010),
+
+            ldap_group("two_user_group", 2012, ["user1", "user2"]),
+        ]
+    )
+    conf_fixture(request, unindent("""\
+        [sssd]
+        debug_level         = 0xffff
+        config_file_version = 2
+        domains             = LDAP
+        services            = nss, pam
+
+        [nss]
+        debug_level         = 0xffff
+
+        [pam]
+        debug_level         = 0xffff
+
+        [domain/LDAP]
+        ldap_auth_disable_tls_never_use_in_production = true
+        debug_level         = 0xffff
+        enumerate           = true
+        ldap_schema         = rfc2307
+        id_provider         = ldap
+        auth_provider       = ldap
+        sudo_provider       = ldap
+        ldap_uri            = %s
+        ldap_search_base    = %s
+    """ % (LDAP_URL, LDAP_BASE_DN)))
+    sssd_fixture(request)
+    return None
+
+@pytest.fixture
+def sanity_rfc2307_bis(request, ldap_conn):
+    ldap_fixture(request, ldap_conn,
+        [
+            ldap_user("user1", 1001, 2001),
+            ldap_user("user2", 1002, 2002),
+            ldap_user("user3", 1003, 2003),
+
+            ldap_group_bis("group1", 2001),
+            ldap_group_bis("group2", 2002),
+            ldap_group_bis("group3", 2003),
+
+            ldap_group_bis("empty_group1", 2010),
+            ldap_group_bis("empty_group2", 2011),
+
+            ldap_group_bis("two_user_group", 2012, ["user1", "user2"]),
+            ldap_group_bis("group_empty_group", 2013, [], ["empty_group1"]),
+            ldap_group_bis("group_two_empty_groups", 2014,
+                           [], ["empty_group1", "empty_group2"]),
+            ldap_group_bis("one_user_group1", 2015, ["user1"]),
+            ldap_group_bis("one_user_group2", 2016, ["user2"]),
+            ldap_group_bis("group_one_user_group", 2017,
+                           [], ["one_user_group1"]),
+            ldap_group_bis("group_two_user_group", 2018,
+                           [], ["two_user_group"]),
+            ldap_group_bis("group_two_one_user_groups", 2019,
+                           [], ["one_user_group1", "one_user_group2"]),
+
+        ]
+    )
+    conf_fixture(request, unindent("""\
+        [sssd]
+        debug_level             = 0xffff
+        config_file_version     = 2
+        domains                 = LDAP
+        services                = nss, pam
+
+        [nss]
+        debug_level             = 0xffff
+
+        [pam]
+        debug_level             = 0xffff
+
+        [domain/LDAP]
+        ldap_auth_disable_tls_never_use_in_production = true
+        debug_level             = 0xffff
+        enumerate               = true
+        ldap_schema             = rfc2307bis
+        ldap_group_object_class = groupOfNames
+        id_provider             = ldap
+        auth_provider           = ldap
+        sudo_provider           = ldap
+        ldap_uri                = %s
+        ldap_search_base        = %s
+    """ % (LDAP_URL, LDAP_BASE_DN)))
+    sssd_fixture(request)
+    return None
+
+def test_sanity_rfc2307(ldap_conn, sanity_rfc2307):
+    assert ent.passwd_list_match(
+            [
+                dict(name='user1', passwd='*', uid=1001, gid=2001, gecos='1001', dir='/home/user1', shell='/bin/bash'),
+                dict(name='user2', passwd='*', uid=1002, gid=2002, gecos='1002', dir='/home/user2', shell='/bin/bash'),
+                dict(name='user3', passwd='*', uid=1003, gid=2003, gecos='1003', dir='/home/user3', shell='/bin/bash')
+            ], True)
+    assert ent.group_list_match(
+            [
+                dict(name='group1', passwd='*', gid=2001, mem=[]),
+                dict(name='group2', passwd='*', gid=2002, mem=[]),
+                dict(name='group3', passwd='*', gid=2003, mem=[]),
+                dict(name='empty_group', passwd='*', gid=2010, mem=[]),
+                dict(name='two_user_group', passwd='*', gid=2012, mem=["user1", "user2"]),
+            ], True)
+    with pytest.raises(KeyError):
+        pwd.getpwnam("non_existent_user")
+    with pytest.raises(KeyError):
+        pwd.getpwuid(1)
+    with pytest.raises(KeyError):
+        grp.getgrnam("non_existent_group")
+    with pytest.raises(KeyError):
+        grp.getgrgid(1)
+
+def test_sanity_rfc2307_bis(ldap_conn, sanity_rfc2307_bis):
+    assert ent.passwd_list_match(
+            [
+                dict(name='user1', passwd='*', uid=1001, gid=2001, gecos='1001', dir='/home/user1', shell='/bin/bash'),
+                dict(name='user2', passwd='*', uid=1002, gid=2002, gecos='1002', dir='/home/user2', shell='/bin/bash'),
+                dict(name='user3', passwd='*', uid=1003, gid=2003, gecos='1003', dir='/home/user3', shell='/bin/bash')
+            ], True)
+    assert ent.group_list_match(
+            [
+                dict(name='group1', passwd='*', gid=2001, mem=[]),
+                dict(name='group2', passwd='*', gid=2002, mem=[]),
+                dict(name='group3', passwd='*', gid=2003, mem=[]),
+                dict(name='empty_group1', passwd='*', gid=2010, mem=[]),
+                dict(name='empty_group2', passwd='*', gid=2011, mem=[]),
+                dict(name='two_user_group', passwd='*', gid=2012, mem=["user1", "user2"]),
+                dict(name='group_empty_group', passwd='*', gid=2013, mem=[]),
+                dict(name='group_two_empty_groups', passwd='*', gid=2014, mem=[]),
+                dict(name='one_user_group1', passwd='*', gid=2015, mem=["user1"]),
+                dict(name='one_user_group2', passwd='*', gid=2016, mem=["user2"]),
+                dict(name='group_one_user_group', passwd='*', gid=2017, mem=["user1"]),
+                dict(name='group_two_user_group', passwd='*', gid=2018, mem=["user1", "user2"]),
+                dict(name='group_two_one_user_groups', passwd='*', gid=2019, mem=["user1", "user2"]),
+            ], True)
+    with pytest.raises(KeyError):
+        pwd.getpwnam("non_existent_user")
+    with pytest.raises(KeyError):
+        pwd.getpwuid(1)
+    with pytest.raises(KeyError):
+        grp.getgrnam("non_existent_group")
+    with pytest.raises(KeyError):
+        grp.getgrgid(1)
diff --git a/src/tests/intg/misc.py b/src/tests/intg/misc.py
new file mode 100644
index 0000000..bd90f7b
--- /dev/null
+++ b/src/tests/intg/misc.py
@@ -0,0 +1,49 @@
+#
+# Various functions
+#
+# Copyright (c) 2015 Red Hat, Inc.
+# Author: Nikolai Kondrashov <Nikolai.Kondrashov@redhat.com>
+#
+# This is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import re
+import os
+import subprocess
+
+UNINDENT_RE = re.compile("^ +", re.MULTILINE)
+
+def unindent(text):
+    """
+        Unindent text by removing at most the number of spaces present in
+        the first non-empty line from the beginning of every line.
+    """
+    indent_ref = [0]
+    def replace(match):
+        if indent_ref[0] == 0:
+            indent_ref[0] = len(match.group())
+        return match.group()[indent_ref[0]:]
+    return UNINDENT_RE.sub(replace, text)
+
+def shell():
+    """
+        Start an interactive shell under "screen", preserving environment.
+        For use as a breakpoint for debugging.
+    """
+    subprocess.call([
+        "screen", "-D", "-m", "bash", "-c",
+        "PATH='" + os.getenv("PATH", "") + "' " +
+        "LD_LIBRARY_PATH='" + os.getenv("LD_LIBRARY_PATH", "") + "' " +
+        "LD_PRELOAD='" + os.getenv("LD_PRELOAD", "") + "' " +
+        "bash -i"
+    ])
diff --git a/src/tests/intg/slapd_setup b/src/tests/intg/slapd_setup
new file mode 100755
index 0000000..e418b1b
--- /dev/null
+++ b/src/tests/intg/slapd_setup
@@ -0,0 +1,246 @@
+#!/bin/bash
+
+set -o errexit -o nounset -o pipefail
+
+# Output usage information.
+function usage()
+{
+    cat <<EOF
+Usage: `basename "$0"` DIR URL_LIST BASE_DN ADMIN_RDN ADMIN_PW
+Setup a slapd instance.
+
+Arguments:
+    DIR         Location of the created instance directory.
+    URL_LIST    List of URLs slapd should bind to, as supplied with -h option.
+    BASE_DN     Base DN.
+    ADMIN_RDN   Administrator DN, relative to BASE_DN.
+    ADMIN_PW    Administrator password.
+
+EOF
+}
+
+# Escape string characters for use in a URL.
+# Args: str
+# Output: escaped string
+function url_encode()
+{
+    declare -r str="$1";    shift
+    declare i
+    declare c
+    for ((i = 0; i < ${#str}; i++)); do
+        c=${str:i:1}
+        if [[ $c == [A-Za-z0-9_.~-] ]]; then
+            printf '%s' "$c"
+        else
+            printf '%%%x' "'$c"
+        fi
+    done
+}
+
+if [ $# != 5 ]; then
+    echo "Invalid number of arguments" >&2
+    usage >&2
+    exit 1
+fi
+
+declare dist_lib_dir=`ls -d /usr/lib/openldap \
+                            /usr/lib64/openldap \
+                            /usr/lib/ldap 2>/dev/null`
+declare dist_conf_dir=`ls -d /etc/ldap /etc/openldap 2>/dev/null`
+
+declare uid
+uid=`id -u`
+declare gid
+gid=`id -g`
+
+declare dir="$1";       shift
+declare url_list="$1";  shift
+declare base_dn="$1";   shift
+declare admin_rdn="$1"; shift
+declare admin_pw="$1";  shift
+
+declare admin_dn="$admin_rdn,$base_dn"
+declare admin_pw_hash
+admin_pw_hash=`slappasswd -s "$admin_pw"`
+
+declare conf_dir="$dir/etc/ldap"
+declare conf_slapd_d_dir="$conf_dir/slapd.d"
+declare run_dir="$dir/var/run/ldap"
+declare pid_file="$run_dir/slapd.pid"
+declare args_file="$run_dir/slapd.args"
+declare data_dir="$dir/var/lib/ldap"
+declare ldapi_socket="$run_dir/ldapi"
+declare ldapi_url="ldapi://"`url_encode "$ldapi_socket"`
+declare try
+
+mkdir -p "$conf_slapd_d_dir" "$run_dir" "$data_dir"
+
+#
+# Add configuration
+#
+cat <<EOF | slapadd -F "$conf_slapd_d_dir" -b "cn=config"
+dn: cn=config
+objectClass: olcGlobal
+cn: config
+olcPidFile: $pid_file
+olcArgsFile: $args_file
+# Read slapd.conf(5) for possible values
+olcLogLevel: none
+
+# Frontend settings
+dn: olcDatabase={-1}frontend,cn=config
+objectClass: olcDatabaseConfig
+objectClass: olcFrontendConfig
+olcDatabase: {-1}frontend
+# The maximum number of entries that is returned for a search operation
+olcSizeLimit: 500
+# Allow unlimited access to local connection from the local root user
+olcAccess: {0}to * by dn.exact=gidNumber=$gid+uidNumber=$uid,cn=peercred,cn=external,cn=auth manage by * break
+# Allow unauthenticated read access for schema and base DN autodiscovery
+olcAccess: {1}to dn.exact="" by * read
+olcAccess: {2}to dn.base="cn=Subschema" by * read
+
+# Config db settings
+dn: olcDatabase=config,cn=config
+objectClass: olcDatabaseConfig
+olcDatabase: config
+# Allow unlimited access to local connection from the local root user
+olcAccess: to * by dn.exact=gidNumber=$gid+uidNumber=$uid,cn=peercred,cn=external,cn=auth manage by * break
+olcRootDN: $admin_rdn,cn=config
+olcRootPW: $admin_pw_hash
+
+# Load schemas
+dn: cn=schema,cn=config
+objectClass: olcSchemaConfig
+cn: schema
+
+include: file://$dist_conf_dir/schema/core.ldif
+include: file://$dist_conf_dir/schema/cosine.ldif
+include: file://$dist_conf_dir/schema/nis.ldif
+include: file://$dist_conf_dir/schema/inetorgperson.ldif
+
+# Load module
+dn: cn=module{0},cn=config
+objectClass: olcModuleList
+cn: module{0}
+olcModulePath: $dist_lib_dir
+olcModuleLoad: back_hdb
+
+# Set defaults for the backend
+dn: olcBackend=hdb,cn=config
+objectClass: olcBackendConfig
+olcBackend: hdb
+
+# The database definition.
+dn: olcDatabase=hdb,cn=config
+objectClass: olcDatabaseConfig
+objectClass: olcHdbConfig
+olcDatabase: hdb
+olcDbCheckpoint: 512 30
+olcLastMod: TRUE
+olcSuffix: $base_dn
+olcDbDirectory: $data_dir
+olcRootDN: $admin_dn
+olcRootPW: $admin_pw_hash
+olcDbIndex: objectClass eq
+olcDbIndex: cn,uid eq
+olcDbIndex: uidNumber,gidNumber eq
+olcDbIndex: member,memberUid eq
+olcAccess: to attrs=userPassword,shadowLastChange
+  by self write
+  by anonymous auth
+  by * none
+olcAccess: to dn.base="" by * read
+olcAccess: to *
+  by * read
+EOF
+
+#
+# Add database config (example from distribution)
+#
+cat >"$data_dir/DB_CONFIG" <<EOF
+# one 0.25 GB cache
+set_cachesize 0 268435456 1
+
+# Transaction Log settings
+set_lg_regionmax 262144
+set_lg_bsize 2097152
+EOF
+
+#
+# Start the daemon
+#
+slapd -F "$conf_slapd_d_dir" -h "$ldapi_url $url_list"
+try=0
+while true; do
+    if [ -e "$pid_file" ]; then
+        break
+    elif ((++try > 30)); then
+        echo "Failed to start slapd" >&2
+        exit 1
+    fi
+    sleep 1
+done
+
+# Relax requirement of member attribute presence in groupOfNames
+cat <<EOF | \
+    ldapmodify -H "$ldapi_url" -x -D $admin_rdn,cn=config -w "$admin_pw" >/dev/null
+dn: cn={0}core,cn=schema,cn=config
+changetype: modify
+delete: olcObjectClasses
+olcObjectClasses: {7}( 2.5.6.9 NAME 'groupOfNames' DESC 'RFC2256:
+  a group of names (DNs)' SUP top STRUCTURAL MUST ( member $ cn )
+  MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )
+-
+add: olcObjectClasses
+olcObjectClasses: {8}( 2.5.6.9 NAME 'groupOfNames' DESC 'RFC2256:
+  a group of names (DNs)' SUP top STRUCTURAL MUST ( cn ) MAY
+  ( member $ businessCategory $ seeAlso $ owner $ ou $ o $
+  description ) )
+-
+EOF
+
+#
+# Add data
+#
+cat <<EOF | \
+    ldapadd -H "$ldapi_url" -x -D "$admin_dn" -w "$admin_pw" >/dev/null
+dn: $base_dn
+objectClass: dcObject
+objectClass: organization
+o: Example Company
+
+dn: cn=Manager,$base_dn
+objectClass: organizationalRole
+cn: Manager
+
+dn: ou=Users,$base_dn
+objectClass: top
+objectClass: organizationalUnit
+ou: Users
+
+dn: ou=Groups,$base_dn
+objectClass: top
+objectClass: organizationalUnit
+ou: Groups
+
+dn: ou=Netgroups,$base_dn
+objectClass: top
+objectClass: organizationalUnit
+ou: Netgroups
+
+dn: ou=Services,$base_dn
+objectClass: top
+objectClass: organizationalUnit
+ou: Services
+
+dn: ou=Pagingtest,$base_dn
+objectClass: top
+objectClass: organizationalUnit
+ou: Pagingtest
+
+dn: ou=Policies,$base_dn
+objectClass: top
+objectClass: organizationalUnit
+ou: policies
+EOF
diff --git a/src/tests/intg/slapd_teardown b/src/tests/intg/slapd_teardown
new file mode 100755
index 0000000..4f7e538
--- /dev/null
+++ b/src/tests/intg/slapd_teardown
@@ -0,0 +1,49 @@
+#!/bin/bash
+
+set -o errexit -o nounset -o pipefail
+
+# Output usage information.
+function usage()
+{
+    cat <<EOF
+Usage: `basename "$0"` DIR
+Teardown a slapd instance.
+
+Arguments:
+    DIR         Location of the instance directory.
+
+EOF
+}
+
+if [ $# != 1 ]; then
+    echo "Invalid number of arguments" >&2
+    usage >&2
+    exit 1
+fi
+
+declare dir="$1";       shift
+declare run_dir="$dir/var/run/ldap"
+declare pid_file="$run_dir/slapd.pid"
+declare pid
+declare try
+
+if pid=`cat "$pid_file" 2>/dev/null`; then
+    kill "$pid"
+    try=0
+    while true; do
+        if ! [ -e "$pid_file" ]; then
+            break
+        elif ((++try > 30)); then
+            echo "Failed to stop slapd" >&2
+            exit 1
+        fi
+        sleep 1
+    done
+fi
+
+declare conf_dir="$dir/etc/ldap"
+declare conf_slapd_d_dir="$conf_dir/slapd.d"
+declare run_dir="$dir/var/run/ldap"
+declare data_dir="$dir/var/lib/ldap"
+
+rm -Rf "$conf_slapd_d_dir" "$run_dir" "$data_dir"
-- 
2.1.4

