>From ce83091f0f9b6609f4559aeac44d383de82856b1 Mon Sep 17 00:00:00 2001
From: Jakub Hrozek <jhrozek@redhat.com>
Date: Tue, 18 Aug 2009 13:34:50 +0200
Subject: [PATCH 3/3] Provide python bindings for sysdb

Implement a set of python bindings for the sysdb with feature set
similar to what is available in the tools. The primary
consumers would be applications like system-config-users.

Resolves: Ticket #102
---
 server/Makefile.am               |   33 ++
 server/conf_macros.m4            |   17 +
 server/configure.ac              |    9 +
 server/external/python.m4        |   49 +++
 server/python/pysss.c            |  704 ++++++++++++++++++++++++++++++++++++++
 server/tests/python-test.py      |  381 +++++++++++++++++++++
 server/tools/common/tools_util.h |    2 +
 server/util/util.h               |    8 +
 8 files changed, 1203 insertions(+), 0 deletions(-)
 create mode 100644 server/external/python.m4
 create mode 100644 server/python/pysss.c
 create mode 100644 server/tests/python-test.py

diff --git a/server/Makefile.am b/server/Makefile.am
index 0a81ab4..f61b091 100644
--- a/server/Makefile.am
+++ b/server/Makefile.am
@@ -85,6 +85,11 @@ libsss_crypt_la_SOURCES = \
 libsss_crypt_la_CPPFLAGS = \
     $(NSS_CFLAGS)
 
+if BUILD_PYTHON_BINDINGS
+pyexec_LTLIBRARIES = \
+    pysss.la
+endif
+
 ###############################
 # Global compilation settings #
 ###############################
@@ -197,6 +202,17 @@ SSSD_LIBS = \
     $(NSS_LIBS) \
     libsss_crypt.la
 
+PYTHON_BINDINGS_LIBS = \
+    $(TALLOC_LIBS) \
+    $(TDB_LIBS) \
+    $(TEVENT_LIBS) \
+    $(POPT_LIBS) \
+    $(LDB_LIBS) \
+    $(REPLACE_LIBS) \
+    $(PCRE_LIBS) \
+    $(NSS_LIBS) \
+    libsss_crypt.la
+
 dist_noinst_HEADERS = \
     monitor/monitor.h \
     util/btreemap.h \
@@ -408,6 +424,7 @@ memberof_la_SOURCES = \
     ldb_modules/memberof.c
 memberof_la_CFLAGS = \
     $(AM_CFLAGS)
+memberof_la_LIBADD = $(LDB_LIBS)
 memberof_la_LDFLAGS = \
     -avoid-version \
     -module
@@ -421,6 +438,22 @@ sssd_krb5_locator_plugin_la_LDFLAGS = \
     -avoid-version \
     -module
 
+if BUILD_PYTHON_BINDINGS
+pysss_la_SOURCES = \
+    $(SSSD_UTIL_OBJ) \
+    $(SSSD_TOOLS_OBJ) \
+    python/pysss.c
+pysss_la_CFLAGS = \
+    $(AM_CFLAGS)  \
+    $(PYTHON_CFLAGS)
+pysss_la_LIBADD = \
+    $(PYTHON_BINDINGS_LIBS) \
+    $(PYTHON_LIBS)
+pysss_la_LDFLAGS = \
+    -avoid-version \
+    -module
+endif
+
 ############
 # MANPAGES #
 ############
diff --git a/server/conf_macros.m4 b/server/conf_macros.m4
index aa2d578..878ca7b 100644
--- a/server/conf_macros.m4
+++ b/server/conf_macros.m4
@@ -137,3 +137,20 @@ AC_DEFUN([WITH_KRB5_PLUGIN_PATH],
     fi
     AC_SUBST(krb5pluginpath)
   ])
+
+AC_DEFUN([WITH_PYTHON_BINDINGS],
+  [ AC_ARG_WITH([python-bindings],
+                [AC_HELP_STRING([--with-python-bindings],
+                                [Whether to build python bindings [no]]
+                               )
+                ],
+                [],
+                with_python_bindings=no
+               )
+    if test x"$with_python_bindings" == xyes; then
+        HAVE_PYTHON_BINDINGS=1
+        AC_SUBST(HAVE_PYTHON_BINDINGS)
+    fi
+  ])
+AM_CONDITIONAL([BUILD_PYTHON_BINDINGS], [test x$with_python_bindings = xyes])
+
diff --git a/server/configure.ac b/server/configure.ac
index d42f07e..b8a5703 100644
--- a/server/configure.ac
+++ b/server/configure.ac
@@ -44,6 +44,7 @@ WITH_SHADOW_UTILS_PATH
 WITH_MANPAGES
 WITH_XML_CATALOG
 WITH_KRB5_PLUGIN_PATH
+WITH_PYTHON_BINDINGS
 
 m4_include([external/pkg.m4])
 m4_include([external/libpopt.m4])
@@ -57,6 +58,7 @@ m4_include([external/libpcre.m4])
 m4_include([external/krb5.m4])
 m4_include([external/libcares.m4])
 m4_include([external/docbook.m4])
+m4_include([external/python.m4])
 m4_include([util/signal.m4])
 
 PKG_CHECK_MODULES([DBUS],[dbus-1])
@@ -76,6 +78,13 @@ if test x$HAVE_MANPAGES != x; then
                      [Docbook XSL templates])
 fi
 
+if test x$HAVE_PYTHON_BINDINGS != x; then
+    AM_PATH_PYTHON([2.4])
+    AM_CHECK_PYTHON_HEADERS([],
+                            AC_MSG_ERROR([Could not find python headers]))
+    AM_PYTHON_CONFIG
+fi
+
 AC_CHECK_HEADERS([sys/inotify.h])
 
 PKG_CHECK_MODULES([CHECK], [check], [have_check=1], [have_check=])
diff --git a/server/external/python.m4 b/server/external/python.m4
new file mode 100644
index 0000000..102afdb
--- /dev/null
+++ b/server/external/python.m4
@@ -0,0 +1,49 @@
+dnl Check for python-config and substitute needed CFLAGS and LDFLAGS
+dnl Usage:
+dnl     AM_PYTHON_CONFIG
+AC_DEFUN([AM_PYTHON_CONFIG],
+[   AC_SUBST(PYTHON_CFLAGS)
+    AC_SUBST(PYTHON_LIBS)
+
+    AC_PATH_PROG(PYTHON_CONFIG, python-config)
+    AC_MSG_CHECKING(for working python-config)
+    if test -x "$PYTHON_CONFIG"; then
+        PYTHON_CFLAGS="`$PYTHON_CONFIG --includes`"
+        PYTHON_CFLAGS=$PYTHON_CFLAGS" -fno-strict-aliasing"
+        PYTHON_LIBS="`$PYTHON_CONFIG --libs`"
+        AC_MSG_RESULT(yes)
+    else
+        AC_MSG_ERROR(no. Please install python devel package)
+    fi
+])
+
+dnl Taken from GNOME sources
+dnl a macro to check for ability to create python extensions
+dnl  AM_CHECK_PYTHON_HEADERS([ACTION-IF-POSSIBLE], [ACTION-IF-NOT-POSSIBLE])
+dnl function also defines PYTHON_INCLUDES
+AC_DEFUN([AM_CHECK_PYTHON_HEADERS],
+[AC_REQUIRE([AM_PATH_PYTHON])
+    AC_MSG_CHECKING(for headers required to compile python extensions)
+
+    dnl deduce PYTHON_INCLUDES
+    py_prefix=`$PYTHON -c "import sys; print sys.prefix"`
+    py_exec_prefix=`$PYTHON -c "import sys; print sys.exec_prefix"`
+    PYTHON_INCLUDES="-I${py_prefix}/include/python${PYTHON_VERSION}"
+    if test "$py_prefix" != "$py_exec_prefix"; then
+        PYTHON_INCLUDES="$PYTHON_INCLUDES -I${py_exec_prefix}/include/python${PYTHON_VERSION}"
+    fi
+
+    AC_SUBST(PYTHON_INCLUDES)
+
+    dnl check if the headers exist:
+    save_CPPFLAGS="$CPPFLAGS"
+    CPPFLAGS="$CPPFLAGS $PYTHON_INCLUDES"
+    AC_TRY_CPP([#include <Python.h>],dnl
+               [AC_MSG_RESULT(found)
+                $1],dnl
+               [AC_MSG_RESULT(not found)
+               $2])
+    CPPFLAGS="$save_CPPFLAGS"
+])
+
+
diff --git a/server/python/pysss.c b/server/python/pysss.c
new file mode 100644
index 0000000..e3d1651
--- /dev/null
+++ b/server/python/pysss.c
@@ -0,0 +1,704 @@
+/*
+ * Authors:
+ *   Jakub Hrozek <jhrozek@redhat.com>
+ *
+ * Copyright (C) 2009  Red Hat
+ * see file 'COPYING' for use and warranty information
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#include <Python.h>
+#include <structmember.h>
+#include <talloc.h>
+#include <pwd.h>
+#include <grp.h>
+
+#include "util/util.h"
+#include "db/sysdb.h"
+#include "tools/common/tools_util.h"
+#include "tools/common/sss_sync_ops.h"
+
+/*
+ * function taken from samba sources tree as of Aug 20 2009,
+ * file source4/lib/ldb/pyldb.c
+ */
+static char **PyList_AsStringList(TALLOC_CTX *mem_ctx, PyObject *list,
+                                        const char *paramname)
+{
+    char **ret;
+    int i;
+    if (!PyList_Check(list)) {
+        PyErr_Format(PyExc_TypeError, "%s is not a list", paramname);
+        return NULL;
+    }
+    ret = talloc_array(NULL, char *, PyList_Size(list)+1);
+    for (i = 0; i < PyList_Size(list); i++) {
+        PyObject *item = PyList_GetItem(list, i);
+        if (!PyString_Check(item)) {
+            PyErr_Format(PyExc_TypeError, "%s should be strings", paramname);
+            return NULL;
+        }
+        ret[i] = talloc_strndup(ret, PyString_AsString(item),
+                PyString_Size(item));
+    }
+    ret[i] = NULL;
+    return ret;
+}
+
+/*
+ * The sss.local object
+ */
+typedef struct {
+    PyObject_HEAD
+    struct tools_ctx *tctx;
+    TALLOC_CTX *mem_ctx;
+    int lock;
+    int unlock;
+} PySssLocalObject;
+
+/*
+ * Error reporting
+ */
+static void PyErr_SetSssErrorWithMessage(int ret, const char *message)
+{
+    PyObject *exc = PyObject_CallFunction(PyExc_IOError,
+                                          discard_const_p(char, "(is)"),
+                                          ret, message);
+
+    PyErr_SetObject(PyExc_IOError, exc);
+    Py_XDECREF(exc);
+}
+
+static void PyErr_SetSssError(int ret)
+{
+    PyErr_SetSssErrorWithMessage(ret, strerror(ret));
+}
+
+static PyObject *py_sss_useradd(PySssLocalObject *self,
+                                PyObject *args,
+                                PyObject *kwds)
+{
+    struct ops_ctx *ops = NULL;
+    uid_t uid = 0;
+    gid_t gid = 0;
+    const char *gecos = NULL;
+    const char *home = NULL;
+    const char *shell = NULL;
+    const char *username = NULL;
+    int ret;
+    const char * const kwlist[] = { "username", "uid", "gid", "gecos",
+                                    "homedir", "shell", "groups", NULL };
+    PyObject *py_groups = Py_None;
+
+    /* parse arguments */
+    if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|kksssO",
+                                     discard_const_p(char *, kwlist),
+                                     &username,
+                                     &uid,
+                                     &gid,
+                                     &gecos,
+                                     &home,
+                                     &shell,
+                                     &py_groups)) {
+        goto fail;
+    }
+
+    ops = talloc_zero(self->mem_ctx, struct ops_ctx);
+    if (ops == NULL) {
+        PyErr_NoMemory();
+        return NULL;
+    }
+    ops->ctx = self->tctx;
+    ops->ev = self->tctx->ev;
+
+    ret = parse_name_domain(ops, username);
+    if (ret != EOK) {
+        PyErr_SetSssErrorWithMessage(ret, "Cannot parse name");
+        goto fail;
+    }
+
+    if (py_groups != Py_None) {
+        ops->addgroups = PyList_AsStringList(self->mem_ctx, py_groups, "groups");
+        if (!ops->addgroups) {
+            return NULL;
+        }
+    }
+
+    ops->uid = uid;
+
+    ret = domain_lookup(ops);
+    if (ret != EOK) {
+        PyErr_SetSssError(ret);
+        goto fail;
+    }
+
+    /* fill in defaults */
+    ret = useradd_defaults(self->mem_ctx, ops,
+                           gecos, home, shell);
+    if (ret != EOK) {
+        PyErr_SetSssError(ret);
+        goto fail;
+    }
+
+    /* useradd */
+    useradd(self->mem_ctx, ops);
+    if (ops->error) {
+        PyErr_SetSssError(ops->error);
+        goto fail;
+    }
+
+    talloc_free(ops);
+    Py_RETURN_NONE;
+
+    /* reachable by jump only */
+fail:
+    talloc_free(ops);
+    return NULL;
+}
+
+static PyObject *py_sss_userdel(PySssLocalObject *self,
+                                PyObject *args,
+                                PyObject *kwds)
+{
+    struct ops_ctx *ops = NULL;
+    const char *username = NULL;
+    struct passwd *pwd_info;
+    int ret;
+
+    if(!PyArg_ParseTuple(args, "s", &username)) {
+        goto fail;
+    }
+
+    ops = talloc_zero(self->mem_ctx, struct ops_ctx);
+    if (ops == NULL) {
+        PyErr_NoMemory();
+        return NULL;
+    }
+    ops->ctx = self->tctx;
+    ops->ev = self->tctx->ev;
+
+    ret = parse_name_domain(ops, username);
+    if (ret != EOK) {
+        PyErr_SetSssErrorWithMessage(ret, "Cannot parse name");
+        goto fail;
+    }
+
+    pwd_info = getpwnam(ops->name);
+    if (pwd_info) {
+        ops->uid = pwd_info->pw_uid;
+    }
+
+    ret = domain_lookup(ops);
+    if (ret != EOK) {
+        PyErr_SetSssError(ret);
+        goto fail;
+    }
+
+    /* userdel */
+    userdel(self->mem_ctx, ops);
+    if (ops->error) {
+        PyErr_SetSssError(ops->error);
+        goto fail;
+    }
+
+    Py_RETURN_NONE;
+
+    /* reachable by jump only */
+fail:
+    talloc_free(ops);
+    return NULL;
+}
+
+static PyObject *py_sss_usermod(PySssLocalObject *self,
+                                PyObject *args,
+                                PyObject *kwds)
+{
+    struct ops_ctx *ops = NULL;
+    int ret;
+    PyObject *py_addgroups = Py_None;
+    PyObject *py_rmgroups = Py_None;
+    uid_t uid = 0;
+    gid_t gid = 0;
+    const char *gecos = NULL;
+    const char *home = NULL;
+    const char *shell = NULL;
+    const char *username = NULL;
+    int lock = 0;
+    const char * const kwlist[] = { "username", "uid", "gid", "gecos",
+                                    "homedir", "shell", "addgroups",
+                                    "rmgroups", "lock", NULL };
+
+    /* parse arguments */
+    if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|kksssOOk",
+                                     discard_const_p(char *, kwlist),
+                                     &username,
+                                     &uid,
+                                     &gid,
+                                     &gecos,
+                                     &home,
+                                     &shell,
+                                     &py_addgroups,
+                                     &py_rmgroups,
+                                     &lock)) {
+        goto fail;
+    }
+
+    ops = talloc_zero(self->mem_ctx, struct ops_ctx);
+    if (ops == NULL) {
+        PyErr_NoMemory();
+        return NULL;
+    }
+    ops->ctx = self->tctx;
+    ops->ev = self->tctx->ev;
+
+    ops->attrs = sysdb_new_attrs(ops->ctx);
+    if (ops->attrs == NULL) {
+        PyErr_NoMemory();
+        return NULL;
+    }
+
+    ret = parse_name_domain(ops, username);
+    if (ret != EOK) {
+        PyErr_SetSssErrorWithMessage(ret, "Cannot parse name");
+        goto fail;
+    }
+
+    if (lock && lock != DO_LOCK && lock != DO_UNLOCK) {
+        PyErr_SetString(PyExc_ValueError,
+                        "Unkown value for lock parameter");
+        goto fail;
+    }
+
+    if (py_addgroups != Py_None) {
+        ops->addgroups = PyList_AsStringList(self->mem_ctx,
+                                             py_addgroups,
+                                             "addgroups");
+        if (!ops->addgroups) {
+            return NULL;
+        }
+    }
+
+    if (py_rmgroups != Py_None) {
+        ops->rmgroups = PyList_AsStringList(self->mem_ctx,
+                                            py_rmgroups,
+                                            "rmgroups");
+        if (!ops->rmgroups) {
+            return NULL;
+        }
+    }
+
+    ret = domain_lookup(ops);
+    if (ret != EOK) {
+        PyErr_SetSssError(ret);
+        goto fail;
+    }
+
+    /* add parameters to changeset */
+    ret = usermod_to_attrs(self->mem_ctx,
+                           ops,
+                           gecos,
+                           home,
+                           shell,
+                           uid,
+                           gid,
+                           lock);
+    if (ret != EOK) {
+        PyErr_SetSssErrorWithMessage(ret, "Could not add attribute to changeset");
+        goto fail;
+    }
+
+    /* usermod */
+    usermod(self->mem_ctx, ops);
+    if (ops->error) {
+        PyErr_SetSssError(ops->error);
+        goto fail;
+    }
+
+    Py_RETURN_NONE;
+
+    /* reachable by jump only */
+fail:
+    talloc_free(ops);
+    return NULL;
+}
+
+static PyObject *py_sss_groupadd(PySssLocalObject *self,
+                                 PyObject *args,
+                                 PyObject *kwds)
+{
+    struct ops_ctx *ops = NULL;
+    const char *groupname;
+    gid_t gid = 0;
+    int ret;
+    const char * const kwlist[] = { "groupname", "gid", NULL };
+
+    /* parse arguments */
+    if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|k",
+                                     discard_const_p(char *, kwlist),
+                                     &groupname,
+                                     &gid)) {
+        goto fail;
+    }
+
+    ops = talloc_zero(self->mem_ctx, struct ops_ctx);
+    if (ops == NULL) {
+        PyErr_NoMemory();
+        return NULL;
+    }
+    ops->ctx = self->tctx;
+    ops->ev = self->tctx->ev;
+
+    ret = parse_name_domain(ops, groupname);
+    if (ret != EOK) {
+        PyErr_SetSssErrorWithMessage(ret, "Cannot parse name");
+        goto fail;
+    }
+
+    ops->gid = gid;
+
+    ret = domain_lookup(ops);
+    if (ret != EOK) {
+        PyErr_SetSssError(ret);
+        goto fail;
+    }
+
+    /* groupadd */
+    groupadd(self->mem_ctx, ops);
+    if (ops->error) {
+        PyErr_SetSssError(ops->error);
+        goto fail;
+    }
+
+    talloc_free(ops);
+    Py_RETURN_NONE;
+
+    /* reachable by jump only */
+fail:
+    talloc_free(ops);
+    return NULL;
+}
+
+static PyObject *py_sss_groupdel(PySssLocalObject *self,
+                                PyObject *args,
+                                PyObject *kwds)
+{
+    struct ops_ctx *ops = NULL;
+    const char *groupname = NULL;
+    struct group *grp_info;
+    int ret;
+
+    if(!PyArg_ParseTuple(args, "s", &groupname)) {
+        goto fail;
+    }
+
+    ops = talloc_zero(self->mem_ctx, struct ops_ctx);
+    if (ops == NULL) {
+        PyErr_NoMemory();
+        return NULL;
+    }
+    ops->ctx = self->tctx;
+    ops->ev = self->tctx->ev;
+
+    ret = parse_name_domain(ops, groupname);
+    if (ret != EOK) {
+        PyErr_SetSssErrorWithMessage(ret, "Cannot parse name");
+        goto fail;
+    }
+
+    grp_info = getgrnam(ops->name);
+    if (grp_info) {
+        ops->gid = grp_info->gr_gid;
+    }
+
+    ret = domain_lookup(ops);
+    if (ret != EOK) {
+        PyErr_SetSssError(ret);
+        goto fail;
+    }
+
+    /* groupdel */
+    groupdel(self->mem_ctx, ops);
+    if (ops->error) {
+        PyErr_SetSssError(ops->error);
+        goto fail;
+    }
+
+    Py_RETURN_NONE;
+
+    /* reachable by jump only */
+fail:
+    talloc_free(ops);
+    return NULL;
+}
+
+static PyObject *py_sss_groupmod(PySssLocalObject *self,
+                                PyObject *args,
+                                PyObject *kwds)
+{
+    struct ops_ctx *ops = NULL;
+    int ret;
+    PyObject *py_addgroups = Py_None;
+    PyObject *py_rmgroups = Py_None;
+    gid_t gid = 0;
+    const char *groupname = NULL;
+    const char * const kwlist[] = { "groupname", "gid", "addgroups",
+                                    "rmgroups", NULL };
+
+    /* parse arguments */
+    if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|kOO",
+                                     discard_const_p(char *, kwlist),
+                                     &groupname,
+                                     &gid,
+                                     &py_addgroups,
+                                     &py_rmgroups)) {
+        goto fail;
+    }
+
+    ops = talloc_zero(self->mem_ctx, struct ops_ctx);
+    if (ops == NULL) {
+        PyErr_NoMemory();
+        return NULL;
+    }
+    ops->ctx = self->tctx;
+    ops->ev = self->tctx->ev;
+
+    ret = parse_name_domain(ops, groupname);
+    if (ret != EOK) {
+        PyErr_SetSssErrorWithMessage(ret, "Cannot parse name");
+        goto fail;
+    }
+
+    if (py_addgroups != Py_None) {
+        ops->addgroups = PyList_AsStringList(self->mem_ctx,
+                                             py_addgroups,
+                                             "addgroups");
+        if (!ops->addgroups) {
+            return NULL;
+        }
+    }
+
+    if (py_rmgroups != Py_None) {
+        ops->rmgroups = PyList_AsStringList(self->mem_ctx,
+                                            py_rmgroups,
+                                            "rmgroups");
+        if (!ops->rmgroups) {
+            return NULL;
+        }
+    }
+    ops->gid = gid;
+
+    ret = domain_lookup(ops);
+    if (ret != EOK) {
+        PyErr_SetSssError(ret);
+        goto fail;
+    }
+
+    /* groupmod */
+    groupmod(self->mem_ctx, ops);
+    if (ops->error) {
+        PyErr_SetSssError(ops->error);
+        goto fail;
+    }
+
+    Py_RETURN_NONE;
+
+    /* reachable by jump only */
+fail:
+    talloc_free(ops);
+    return NULL;
+}
+
+
+/*** python plumbing begins here ***/
+
+/*
+ * The sss.local destructor
+ */
+static void PySssLocalObject_dealloc(PySssLocalObject *self)
+{
+    talloc_free(self->mem_ctx);
+    self->ob_type->tp_free((PyObject*) self);
+}
+
+/*
+ * The sss.local constructor
+ */
+static PyObject *PySssLocalObject_new(PyTypeObject *type,
+                                      PyObject *args,
+                                      PyObject *kwds)
+{
+    TALLOC_CTX *mem_ctx;
+    PySssLocalObject *self;
+    int ret;
+
+    mem_ctx = talloc_new(NULL);
+    if (mem_ctx == NULL) {
+        PyErr_NoMemory();
+        return NULL;
+    }
+
+    self = (PySssLocalObject *) type->tp_alloc(type, 0);
+    if (self == NULL) {
+        talloc_free(mem_ctx);
+        PyErr_NoMemory();
+        return NULL;
+    }
+    self->mem_ctx = mem_ctx;
+
+    /* Connect to the database */
+    ret = setup_db(&self->tctx);
+    if (ret != EOK) {
+        PyErr_SetSssErrorWithMessage(ret, "Cannot connect do database");
+        return NULL;
+    }
+
+    ret = sss_names_init(self->tctx, self->tctx->confdb, &self->tctx->snctx);
+    if (ret != EOK) {
+        PyErr_SetSssErrorWithMessage(ret, "Cannot initialize parsing");
+        return NULL;
+    }
+
+    self->lock = DO_LOCK;
+    self->unlock = DO_UNLOCK;
+
+    return (PyObject *) self;
+}
+
+/*
+ * sss.local object methods
+ */
+static PyMethodDef sss_local_methods[] = {
+    {"useradd", (PyCFunction) py_sss_useradd, METH_KEYWORDS,
+        "Add a user.\n"
+        "@param username: Name of user being added\n"
+        "@type username: string\n"
+        "useradd can be customized further with keyword arguments:\n"
+        "@param uid: The UID of the user\n"
+        "@type uid: integer\n"
+        "@param gid: The GID of the user\n"
+        "@type gid: integer\n"
+        "@param gecos: The comment string\n"
+        "@type gecos: string \n"
+        "@param homedir: Home directory\n"
+        "@type homedir: string\n"
+        "@param shell: Login shell\n"
+        "@type shell: string\n"
+        "@param groups: Groups the user is a member of\n"
+        "@type groups: string\n"
+    },
+    {"userdel", (PyCFunction) py_sss_userdel, METH_VARARGS,
+        "Remove a user.\n"
+        "@param username: Name of user being removed\n"
+        "@type username: string\n"
+    },
+    {"usermod", (PyCFunction) py_sss_usermod, METH_KEYWORDS,
+        "Modify a user.\n"
+        "@param username: Name of user being modified\n"
+        "@type username: string\n"
+        "usermod can be customized further with keyword arguments:\n"
+        "@param uid: The UID of the user\n"
+        "@type uid: integer\n"
+        "@param gid: The GID of the user\n"
+        "@type gid: integer\n"
+        "@param gecos: The comment string\n"
+        "@type gecos: string \n"
+        "@param homedir: Home directory\n"
+        "@type homedir: string\n"
+        "@param shell: Login shell\n"
+        "@type shell: string\n"
+        "@param addgroups: Groups to add the user to\n"
+        "@type addgroups: string\n"
+        "@param rmgroups: Groups to remove the user from\n"
+        "@type rmgroups: string\n"
+        "@param lock: Lock or unlock the account\n"
+        "@type lock: Can be one of pysss.local.lock or pysss.local.unlock\n"
+    },
+    {"groupadd", (PyCFunction) py_sss_groupadd, METH_KEYWORDS,
+        "Add a group.\n"
+        "@param groupname: Name of group being added\n"
+        "@type groupname: string\n"
+        "groupadd can be customized further with keyword arguments:\n"
+        "@param gid: The GID of the group\n"
+        "@type gid: integer\n"
+    },
+    {"groupdel", (PyCFunction) py_sss_groupdel, METH_VARARGS,
+        "Remove a group.\n"
+        "@param groupname: Name of group being removed\n"
+        "@type groupname: string\n"
+    },
+    {"groupmod", (PyCFunction) py_sss_groupmod, METH_KEYWORDS,
+        "Modify a group.\n"
+        "@param groupname: Name of group being modified\n"
+        "groupmod can be customized further with keyword arguments:\n"
+        "@type groupname: string\n"
+        "@param gid: The GID of the group\n"
+        "@type gid: integer\n"
+        "@param addgroups: Groups to add the group to\n"
+        "@type addgroups: string\n"
+        "@param rmgroups: Groups to remove the group from\n"
+        "@type rmgroups: string\n"
+    },
+    {NULL, NULL, 0, NULL}        /* Sentinel */
+};
+
+static PyMemberDef sss_members[] = {
+    { discard_const_p(char, "lock"), T_INT,
+      offsetof(PySssLocalObject, lock), RO },
+    { discard_const_p(char, "unlock"), T_INT,
+      offsetof(PySssLocalObject, unlock), RO },
+    {NULL} /* Sentinel */
+};
+
+/*
+ * sss.local object properties
+ */
+static PyTypeObject pysss_local_type = {
+    PyObject_HEAD_INIT(NULL)
+    .tp_name = "sss.local",
+    .tp_basicsize = sizeof(PySssLocalObject),
+    .tp_new = PySssLocalObject_new,
+    .tp_dealloc = (destructor) PySssLocalObject_dealloc,
+    .tp_methods = sss_local_methods,
+    .tp_members = sss_members,
+    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+    .tp_doc   = "SSS DB manipulation",
+};
+
+/*
+ * Module methods
+ */
+static PyMethodDef module_methods[] = {
+        {NULL}  /* Sentinel */
+};
+
+/*
+ * Module initialization
+ */
+PyMODINIT_FUNC
+initpysss(void)
+{
+    PyObject *m;
+
+    if (PyType_Ready(&pysss_local_type) < 0)
+        return;
+
+    m = Py_InitModule("pysss", module_methods);
+    if (m == NULL)
+        return;
+
+    Py_INCREF(&pysss_local_type);
+    PyModule_AddObject(m, "local", (PyObject *)&pysss_local_type);
+}
+
diff --git a/server/tests/python-test.py b/server/tests/python-test.py
new file mode 100644
index 0000000..6c062ac
--- /dev/null
+++ b/server/tests/python-test.py
@@ -0,0 +1,381 @@
+#!/usr/bin/python
+#coding=utf-8
+
+# Authors:
+#   Jakub Hrozek <jhrozek@redhat.com>
+#
+# Copyright (C) 2009  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import unittest
+import commands
+import errno
+
+# module under test
+import pysss
+
+class LocalTest(unittest.TestCase):
+    local_path = "/var/lib/sss/db/sssd.ldb"
+
+    def setUp(self):
+        self.local = pysss.local()
+
+    def _run_and_check(self, runme):
+        (status, output) = commands.getstatusoutput(runme)
+        self.failUnlessEqual(status, 0, output)
+
+    def _get_object_info(self, name, subtree, domain):
+        search_dn = "dn=name=%s,cn=%s,cn=%s,cn=sysdb" % (name, subtree, domain)
+        (status, output) = commands.getstatusoutput("ldbsearch -H %s %s" % (self.local_path,search_dn))
+
+        if status: return {}
+
+        kw = {}
+        for key, value in [ l.split(':') for l in output.split('\n') if ":" in l ]:
+            kw[key] = value.strip()
+
+        del kw['asq']
+        return kw
+
+    def get_user_info(self, name, domain="LOCAL"):
+        return self._get_object_info(name, "users", domain)
+
+    def get_group_info(self, name, domain="LOCAL"):
+        return self._get_object_info(name, "groups", domain)
+
+    def _validate_object(self, kw, name, **kwargs):
+        if kw == {}: self.fail("Could not get %s info" % name)
+        for key in kwargs.keys():
+            self.assert_(str(kwargs[key]) == str(kw[key]), "%s %s != %s %s" % (key, kwargs[key], key, kw[key]))
+
+    def validate_user(self, username, **kwargs):
+        return self._validate_object(self.get_user_info(username), "user", **kwargs)
+
+    def validate_group(self, groupname, **kwargs):
+        return self._validate_object(self.get_group_info(groupname), "group", **kwargs)
+
+    def _validate_no_object(self, kw, name):
+        if kw != {}:
+            self.fail("Got %s info" % name)
+
+    def validate_no_user(self, username):
+        return self._validate_no_object(self.get_user_info(username), "user")
+
+    def validate_no_group(self, groupname):
+        return self._validate_no_object(self.get_group_info(groupname), "group")
+
+    def _get_object_membership(self, name, subtree, domain):
+        search_dn = "dn=name=%s,cn=%s,cn=%s,cn=sysdb" % (name, subtree, domain)
+        (status, output) = commands.getstatusoutput("ldbsearch -H %s %s" % (self.local_path,search_dn))
+
+        if status:
+            return []
+
+        members = [ value.strip() for key, value in [ l.split(':') for l in output.split('\n') if ":" in l ] if key == "memberof" ]
+        return members
+
+    def _assertMembership(self, name, group_list, subtree, domain):
+        members = self._get_object_membership(name, subtree, domain)
+        for group in group_list:
+            group_dn = "name=%s,cn=groups,cn=%s,cn=sysdb" % (group, domain)
+            if group_dn in members:
+                members.remove(group_dn)
+            else:
+                self.fail("Cannot find required group %s" % group_dn)
+
+        if len(members) > 0:
+            self.fail("More groups than selected")
+
+    def assertUserMembership(self, name, group_list, domain="LOCAL"):
+        return self._assertMembership(name, group_list, "users", domain)
+
+    def assertGroupMembership(self, name, group_list, domain="LOCAL"):
+        return self._assertMembership(name, group_list, "groups", domain)
+
+    def get_user_membership(self, name, domain="LOCAL"):
+        return self._get_object_membership(name, "users", domain)
+
+    def get_group_membership(self, name, domain="LOCAL"):
+        return self._get_object_membership(name, "groups", domain)
+
+    def add_group(self, groupname):
+        self._run_and_check("sss_groupadd %s" % (groupname))
+
+    def remove_group(self, groupname):
+        self._run_and_check("sss_groupdel %s" % (groupname))
+
+    def add_user(self, username):
+        self._run_and_check("sss_useradd %s" % (username))
+
+    def remove_user(self, username):
+        self._run_and_check("sss_userdel %s" % (username))
+
+class SanityTest(unittest.TestCase):
+    def testInstantiate(self):
+        "Test that the local backed binding can be instantiated"
+        local = pysss.local()
+        self.assert_(local.__class__, "<type 'sss.local'>")
+
+class UseraddTest(LocalTest):
+    def tearDown(self):
+        if self.username:
+            self.remove_user(self.username)
+
+    def testUseradd(self):
+        "Test adding a local user"
+        self.username = "testUseradd"
+        self.local.useradd(self.username)
+        self.validate_user(self.username)
+
+    def testUseraddWithParams(self):
+        "Test adding a local user with modified parameters"
+        self.username = "testUseraddWithParams"
+        self.local.useradd(self.username,
+                           gecos="foo bar",
+                           homedir="/people/foobar",
+                           shell="/bin/zsh")
+        self.validate_user(self.username,
+                           gecos="foo bar",
+                           homeDirectory="/people/foobar",
+                           loginShell="/bin/zsh")
+
+    def testUseraddToGroups(self):
+        "Test adding a local user with group membership"
+        self.username = "testUseraddToGroups"
+        self.add_group("gr1")
+        self.add_group("gr2")
+        try:
+            self.local.useradd(self.username,
+                               groups=["gr1","gr2"])
+            self.assertUserMembership(self.username,
+                                      ["gr1","gr2"])
+        finally:
+            self.remove_group("gr1")
+            self.remove_group("gr2")
+
+    def testUseraddWithUID(self):
+        "Test adding a local user with a custom UID"
+        self.username = "testUseraddWithUID"
+        self.local.useradd(self.username,
+                           uid=1024)
+        self.validate_user(self.username,
+                           uidNumber=1024)
+
+class UseraddTestNegative(LocalTest):
+    def testUseraddNoParams(self):
+        "Test that local.useradd() requires the username parameter"
+        self.assertRaises(TypeError, self.local.useradd)
+
+    def testUseraddNoSuchDomain(self):
+        "Test adding a local user into domain that does not exist"
+        self.username = "testUseraddNoSuchDomain@ABCXYZ"
+        try:
+            self.local.useradd(self.username)
+        except IOError, e:
+            self.assertEquals(e.errno, errno.EINVAL)
+        else:
+            fail("Was expecting exception")
+
+    def testUseraddUserAlreadyExists(self):
+        "Test adding a local with a duplicite name"
+        self.username = "testUseraddUserAlreadyExists"
+        self.local.useradd(self.username)
+        try:
+            self.local.useradd(self.username)
+        except IOError, e:
+            self.assertEquals(e.errno, errno.EEXIST)
+        else:
+            self.fail("Was expecting exception")
+        finally:
+            self.remove_user(self.username)
+
+    def testUseraddUIDAlreadyExists(self):
+        "Test adding a local with a duplicite user ID"
+        self.username = "testUseraddUIDAlreadyExists1"
+        self.local.useradd(self.username, uid=1025)
+        try:
+            self.local.useradd("testUseraddUIDAlreadyExists2", uid=1025)
+        except IOError, e:
+            self.assertEquals(e.errno, errno.EEXIST)
+        else:
+            self.fail("Was expecting exception")
+        finally:
+            self.remove_user(self.username)
+
+class UserdelTest(LocalTest):
+    def testUserdel(self):
+        self.add_user("testUserdel")
+        self.validate_user("testUserdel")
+        self.local.userdel("testUserdel")
+        self.validate_no_user("testUserdel")
+
+    def testUserdelNegative(self):
+        self.validate_no_user("testUserdelNegative")
+        try:
+            self.local.userdel("testUserdelNegative")
+        except IOError, e:
+            self.assertEquals(e.errno, errno.ENOENT)
+        else:
+            fail("Was expecting exception")
+
+class UsermodTest(LocalTest):
+    def setUp(self):
+        self.local = pysss.local()
+        self.username = "UsermodTest"
+        self.add_user(self.username)
+
+    def tearDown(self):
+        self.remove_user(self.username)
+
+    def testUsermod(self):
+        "Test modifying user attributes"
+        self.local.usermod(self.username,
+                           gecos="foo bar",
+                           homedir="/people/foobar",
+                           shell="/bin/zsh")
+        self.validate_user(self.username,
+                           gecos="foo bar",
+                           homeDirectory="/people/foobar",
+                           loginShell="/bin/zsh")
+
+    def testUsermodUID(self):
+        "Test modifying UID"
+        self.local.usermod(self.username,
+                           uid=1024)
+        self.validate_user(self.username,
+                           uidNumber=1024)
+
+    def testUsermodGroupMembership(self):
+        "Test adding to groups"
+        self.add_group("gr1")
+        self.add_group("gr2")
+
+        try:
+            self.local.usermod(self.username,
+                               addgroups=["gr1","gr2"])
+            self.assertUserMembership(self.username,
+                                      ["gr1","gr2"])
+            self.local.usermod(self.username,
+                               rmgroups=["gr2"])
+            self.assertUserMembership(self.username,
+                                      ["gr1"])
+            self.local.usermod(self.username,
+                               rmgroups=["gr1"])
+            self.assertUserMembership(self.username,
+                                      [])
+        finally:
+            self.remove_group("gr1")
+            self.remove_group("gr2")
+
+    def testUsermodLockUnlock(self):
+        "Test locking and unlocking user"
+        self.local.usermod(self.username,
+                           lock=self.local.lock)
+        self.validate_user(self.username,
+                           disabled="true")
+        self.local.usermod(self.username,
+                           lock=self.local.unlock)
+        self.validate_user(self.username,
+                           disabled="false")
+
+class GroupaddTest(LocalTest):
+    def tearDown(self):
+        if self.groupname:
+            self.remove_group(self.groupname)
+
+    def testGroupadd(self):
+        "Test adding a local group"
+        self.groupname = "testGroupadd"
+        self.local.groupadd(self.groupname)
+        self.validate_group(self.groupname)
+
+    def testGroupaddWithGID(self):
+        "Test adding a local group with a custom GID"
+        self.groupname = "testUseraddWithGID"
+        self.local.groupadd(self.groupname,
+                           gid=1024)
+        self.validate_group(self.groupname,
+                           gidNumber=1024)
+
+class GroupaddTestNegative(LocalTest):
+    def testGroupaddNoParams(self):
+        "Test that local.groupadd() requires the groupname parameter"
+        self.assertRaises(TypeError, self.local.groupadd)
+
+    def testGroupaddNoSuchDomain(self):
+        "Test adding a local group into domain that does not exist"
+        self.groupname = "testGroupaddNoSuchDomain@ABCXYZ"
+        try:
+            self.local.groupadd(self.groupname)
+        except IOError, e:
+            self.assertEquals(e.errno, errno.EINVAL)
+        else:
+            fail("Was expecting exception")
+
+    def testGroupaddUserAlreadyExists(self):
+        "Test adding a local with a duplicite name"
+        self.groupname = "testGroupaddUserAlreadyExists"
+        self.local.groupadd(self.groupname)
+        try:
+            self.local.groupadd(self.groupname)
+        except IOError, e:
+            self.assertEquals(e.errno, errno.EEXIST)
+        else:
+            self.fail("Was expecting exception")
+        finally:
+            self.remove_group(self.groupname)
+
+    def testGroupaddGIDAlreadyExists(self):
+        "Test adding a local with a duplicite group ID"
+        self.groupname = "testGroupaddGIDAlreadyExists1"
+        self.local.groupadd(self.groupname, gid=1025)
+        try:
+            self.local.groupadd("testGroupaddGIDAlreadyExists2", gid=1025)
+        except IOError, e:
+            self.assertEquals(e.errno, errno.EEXIST)
+        else:
+            self.fail("Was expecting exception")
+        finally:
+            self.remove_group(self.groupname)
+
+class GroupmodTest(LocalTest):
+    def setUp(self):
+        self.local = pysss.local()
+        self.groupname = "GroupmodTest"
+        self.add_group(self.groupname)
+
+    def tearDown(self):
+        self.remove_group(self.groupname)
+
+    def testGroupmodGID(self):
+        "Test modifying UID"
+        self.local.groupmod(self.groupname,
+                           gid=1024)
+        self.validate_group(self.groupname,
+                           gidNumber=1024)
+
+    def testGroupmodAddGroups(self):
+        "Test adding to groups"
+        pass
+
+    def testGroupmodRemoveGroups(self):
+        "Test removing from groups"
+        pass
+
+# -------------- run the test suite -------------- #
+if __name__ == "__main__":
+    unittest.main()
+
diff --git a/server/tools/common/tools_util.h b/server/tools/common/tools_util.h
index 604a6ac..3252685 100644
--- a/server/tools/common/tools_util.h
+++ b/server/tools/common/tools_util.h
@@ -1,6 +1,8 @@
 #ifndef __TOOLS_UTIL_H__
 #define __TOOLS_UTIL_H__
 
+#include <popt.h>
+
 #include "util/sssd-i18n.h"
 #include "tools/common/sss_sync_ops.h"
 
diff --git a/server/util/util.h b/server/util/util.h
index 8796529..ab095cd 100644
--- a/server/util/util.h
+++ b/server/util/util.h
@@ -79,6 +79,14 @@ void debug_fn(const char *format, ...);
 #define talloc_zfree(ptr) do { talloc_free(ptr); ptr = NULL; } while(0)
 #endif
 
+#ifndef discard_const_p
+#if defined(__intptr_t_defined) || defined(HAVE_INTPTR_T)
+# define discard_const_p(type, ptr) ((type *)((intptr_t)(ptr)))
+#else
+# define discard_const_p(type, ptr) ((type *)(ptr))
+#endif
+#endif
+
 /* TODO: remove later
  * These functions are available in the latest tevent and are the ones that
  * should be used as tevent_req is rightfully opaque there */
-- 
1.6.2.5

