diff options
Diffstat (limited to 'src/python')
-rw-r--r-- | src/python/pysss.c | 937 |
1 files changed, 937 insertions, 0 deletions
diff --git a/src/python/pysss.c b/src/python/pysss.c new file mode 100644 index 00000000..8011ed67 --- /dev/null +++ b/src/python/pysss.c @@ -0,0 +1,937 @@ +/* + Authors: + Jakub Hrozek <jhrozek@redhat.com> + + Copyright (C) 2009 Red Hat + + 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; either version 3 of the License, or + (at your option) any later version. + + 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/>. +*/ + +#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/tools_util.h" +#include "tools/sss_sync_ops.h" + +#define TRANSACTION_WAIT(trs, retval) do { \ + while (!trs->transaction_done) { \ + tevent_loop_once(trs->self->ev); \ + } \ + retval = trs->error; \ + if (retval) { \ + PyErr_SetSssError(retval); \ + goto fail; \ + } \ +} while(0) + +/* + * 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; + + 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 + + TALLOC_CTX *mem_ctx; + struct tevent_context *ev; + struct sysdb_ctx *sysdb; + struct confdb_ctx *confdb; + + struct sss_domain_info *local; + + int lock; + int unlock; +} PySssLocalObject; + +/* + * The transaction object + */ +struct py_sss_transaction { + PySssLocalObject *self; + struct ops_ctx *ops; + + struct sysdb_handle *handle; + bool transaction_done; + int error; +}; + +/* + * Error reporting + */ +static void PyErr_SetSssErrorWithMessage(int ret, const char *message) +{ + PyObject *exc = Py_BuildValue(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)); +} + +/* + * Common init of all methods + */ +struct tools_ctx *init_ctx(TALLOC_CTX *mem_ctx, + PySssLocalObject *self) +{ + struct ops_ctx *octx = NULL; + struct tools_ctx *tctx = NULL; + + tctx = talloc_zero(self->mem_ctx, struct tools_ctx); + if (tctx == NULL) { + return NULL; + } + + tctx->ev = self->ev; + tctx->confdb = self->confdb; + tctx->sysdb = self->sysdb; + tctx->local = self->local; + /* tctx->nctx is NULL here, which is OK since we don't parse domains + * in the python bindings (yet?) */ + + octx = talloc_zero(tctx, struct ops_ctx); + if (octx == NULL) { + PyErr_NoMemory(); + return NULL; + } + octx->domain = self->local; + + tctx->octx = octx; + return tctx; +} + +/* + * Add a user + */ +PyDoc_STRVAR(py_sss_useradd__doc__, + "Add a user named ``username``.\n\n" + ":param username: name of the user\n\n" + ":param kwargs: Keyword arguments that customize the operation\n\n" + "* useradd can be customized further with keyword arguments:\n" + " * ``uid``: The UID of the user\n" + " * ``gid``: The GID of the user\n" + " * ``gecos``: The comment string\n" + " * ``homedir``: Home directory\n" + " * ``shell``: Login shell\n" + " * ``skel``: Specify an alternative skeleton directory\n" + " * ``create_home``: (bool) Force creation of home directory on or off\n" + " * ``groups``: List of groups the user is member of\n"); + + +static PyObject *py_sss_useradd(PySssLocalObject *self, + PyObject *args, + PyObject *kwds) +{ + struct tools_ctx *tctx = NULL; + unsigned long uid = 0; + unsigned long gid = 0; + const char *gecos = NULL; + const char *home = NULL; + const char *shell = NULL; + const char *skel = NULL; + char *username = NULL; + int ret; + const char * const kwlist[] = { "username", "uid", "gid", "gecos", + "homedir", "shell", "skel", + "create_home", "groups", NULL }; + PyObject *py_groups = Py_None; + PyObject *py_create_home = Py_None; + int create_home = 0; + + /* parse arguments */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, + discard_const_p(char, "s|kkssssO!O!"), + discard_const_p(char *, kwlist), + &username, + &uid, + &gid, + &gecos, + &home, + &shell, + &skel, + &PyBool_Type, + &py_create_home, + &PyList_Type, + &py_groups)) { + goto fail; + } + + tctx = init_ctx(self->mem_ctx, self); + if (!tctx) { + PyErr_NoMemory(); + return NULL; + } + + if (py_groups != Py_None) { + tctx->octx->addgroups = PyList_AsStringList(tctx, py_groups, "groups"); + if (!tctx->octx->addgroups) { + PyErr_NoMemory(); + return NULL; + } + } + + /* user-wise the parameter is only bool - do or don't, + * however we must have a third state - undecided, pick default */ + if (py_create_home == Py_True) { + create_home = DO_CREATE_HOME; + } else if (py_create_home == Py_False) { + create_home = DO_NOT_CREATE_HOME; + } + + tctx->octx->name = username; + tctx->octx->uid = uid; + + /* fill in defaults */ + ret = useradd_defaults(tctx, + self->confdb, + tctx->octx, gecos, + home, shell, + create_home, + skel); + if (ret != EOK) { + PyErr_SetSssError(ret); + goto fail; + } + + /* Add the user within a transaction */ + start_transaction(tctx); + if (tctx->error != EOK) { + PyErr_SetSssError(tctx->error); + goto fail; + } + + /* useradd */ + ret = useradd(tctx, self->ev, + self->sysdb, tctx->handle, tctx->octx); + if (ret != EOK) { + tctx->error = ret; + + /* cancel transaction */ + talloc_zfree(tctx->handle); + PyErr_SetSssError(tctx->error); + goto fail; + } + + end_transaction(tctx); + if (tctx->error) { + PyErr_SetSssError(tctx->error); + goto fail; + } + + /* Create user's home directory and/or mail spool */ + if (tctx->octx->create_homedir) { + /* We need to know the UID and GID of the user, if + * sysdb did assign it automatically, do a lookup */ + if (tctx->octx->uid == 0 || tctx->octx->gid == 0) { + ret = sysdb_getpwnam_sync(tctx, + tctx->ev, + tctx->sysdb, + tctx->octx->name, + tctx->local, + &tctx->octx); + if (ret != EOK) { + PyErr_SetSssError(ret); + goto fail; + } + } + + ret = create_homedir(tctx, + tctx->octx->skeldir, + tctx->octx->home, + tctx->octx->name, + tctx->octx->uid, + tctx->octx->gid, + tctx->octx->umask); + if (ret != EOK) { + PyErr_SetSssError(ret); + goto fail; + } + + /* failure here should not be fatal */ + create_mail_spool(tctx, + tctx->octx->name, + tctx->octx->maildir, + tctx->octx->uid, + tctx->octx->gid); + } + + talloc_zfree(tctx); + Py_RETURN_NONE; + +fail: + talloc_zfree(tctx); + return NULL; +} + +/* + * Delete a user + */ +PyDoc_STRVAR(py_sss_userdel__doc__, + "Remove the user named ``username``.\n\n" + ":param username: Name of user being removed\n" + ":param kwargs: Keyword arguments that customize the operation\n\n" + "* userdel can be customized further with keyword arguments:\n" + " * ``force``: (bool) Force removal of files not owned by the user\n" + " * ``remove``: (bool) Toggle removing home directory and mail spool\n"); + +static PyObject *py_sss_userdel(PySssLocalObject *self, + PyObject *args, + PyObject *kwds) +{ + struct tools_ctx *tctx = NULL; + char *username = NULL; + int ret; + PyObject *py_remove = Py_None; + int remove_home = 0; + PyObject *py_force = Py_None; + const char * const kwlist[] = { "username", "remove", "force", NULL }; + + if(!PyArg_ParseTupleAndKeywords(args, kwds, + discard_const_p(char, "s|O!O!"), + discard_const_p(char *, kwlist), + &username, + &PyBool_Type, + &py_remove, + &PyBool_Type, + &py_force)) { + goto fail; + } + + tctx = init_ctx(self->mem_ctx, self); + if (!tctx) { + PyErr_NoMemory(); + return NULL; + } + + tctx->octx->name = username; + + if (py_remove == Py_True) { + remove_home = DO_REMOVE_HOME; + } else if (py_remove == Py_False) { + remove_home = DO_NOT_REMOVE_HOME; + } + + /* + * Fills in defaults for ops_ctx user did not specify. + */ + ret = userdel_defaults(tctx, + tctx->confdb, + tctx->octx, + remove_home); + if (ret != EOK) { + PyErr_SetSssError(ret); + goto fail; + } + + if (tctx->octx->remove_homedir) { + ret = sysdb_getpwnam_sync(tctx, + tctx->ev, + tctx->sysdb, + tctx->octx->name, + tctx->local, + &tctx->octx); + if (ret != EOK) { + PyErr_SetSssError(ret); + goto fail; + } + } + + /* Delete the user within a transaction */ + start_transaction(tctx); + if (tctx->error != EOK) { + PyErr_SetSssError(tctx->error); + goto fail; + } + + ret = userdel(tctx, self->ev, + self->sysdb, tctx->handle, tctx->octx); + if (ret != EOK) { + tctx->error = ret; + + /* cancel transaction */ + talloc_zfree(tctx->handle); + PyErr_SetSssError(tctx->error); + goto fail; + } + + end_transaction(tctx); + if (tctx->error) { + PyErr_SetSssError(tctx->error); + goto fail; + } + + if (tctx->octx->remove_homedir) { + ret = remove_homedir(tctx, + tctx->octx->home, + tctx->octx->maildir, + tctx->octx->name, + tctx->octx->uid, + (py_force == Py_True)); + if (ret != EOK) { + PyErr_SetSssError(ret); + goto fail; + } + } + + talloc_zfree(tctx); + Py_RETURN_NONE; + +fail: + talloc_zfree(tctx); + return NULL; +} + +/* + * Modify a user + */ +PyDoc_STRVAR(py_sss_usermod__doc__, + "Modify a user.\n\n" + ":param username: Name of user being modified\n\n" + ":param kwargs: Keyword arguments that customize the operation\n\n" + "* usermod can be customized further with keyword arguments:\n" + " * ``uid``: The UID of the user\n" + " * ``gid``: The GID of the user\n" + " * ``gecos``: The comment string\n" + " * ``homedir``: Home directory\n" + " * ``shell``: Login shell\n" + " * ``addgroups``: List of groups to add the user to\n" + " * ``rmgroups``: List of groups to remove the user from\n" + " * ``lock``: Lock or unlock the account\n"); + +static PyObject *py_sss_usermod(PySssLocalObject *self, + PyObject *args, + PyObject *kwds) +{ + struct tools_ctx *tctx = NULL; + int ret; + PyObject *py_addgroups = Py_None; + PyObject *py_rmgroups = Py_None; + unsigned long uid = 0; + unsigned long gid = 0; + char *gecos = NULL; + char *home = NULL; + char *shell = NULL; + char *username = NULL; + unsigned long lock = 0; + const char * const kwlist[] = { "username", "uid", "gid", "lock", + "gecos", "homedir", "shell", + "addgroups", "rmgroups", NULL }; + + /* parse arguments */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, + discard_const_p(char, "s|kkksssO!O!"), + discard_const_p(char *, kwlist), + &username, + &uid, + &gid, + &lock, + &gecos, + &home, + &shell, + &PyList_Type, + &py_addgroups, + &PyList_Type, + &py_rmgroups)) { + goto fail; + } + + tctx = init_ctx(self->mem_ctx, self); + if (!tctx) { + PyErr_NoMemory(); + return NULL; + } + + if (lock && lock != DO_LOCK && lock != DO_UNLOCK) { + PyErr_SetString(PyExc_ValueError, + "Unkown value for lock parameter"); + goto fail; + } + + if (py_addgroups != Py_None) { + tctx->octx->addgroups = PyList_AsStringList(tctx, + py_addgroups, + "addgroups"); + if (!tctx->octx->addgroups) { + return NULL; + } + } + + if (py_rmgroups != Py_None) { + tctx->octx->rmgroups = PyList_AsStringList(tctx, + py_rmgroups, + "rmgroups"); + if (!tctx->octx->rmgroups) { + return NULL; + } + } + + tctx->octx->name = username; + tctx->octx->uid = uid; + tctx->octx->gid = gid; + tctx->octx->gecos = gecos; + tctx->octx->home = home; + tctx->octx->shell = shell; + tctx->octx->lock = lock; + + /* Modify the user within a transaction */ + start_transaction(tctx); + if (tctx->error != EOK) { + PyErr_SetSssError(tctx->error); + goto fail; + } + + /* usermod */ + ret = usermod(tctx, self->ev, + self->sysdb, tctx->handle, tctx->octx); + if (ret != EOK) { + tctx->error = ret; + + /* cancel transaction */ + talloc_zfree(tctx->handle); + PyErr_SetSssError(tctx->error); + goto fail; + } + + end_transaction(tctx); + if (tctx->error) { + PyErr_SetSssError(tctx->error); + goto fail; + } + + talloc_zfree(tctx); + Py_RETURN_NONE; + +fail: + talloc_zfree(tctx); + return NULL; +} + +/* + * Add a group + */ +PyDoc_STRVAR(py_sss_groupadd__doc__, + "Add a group.\n\n" + ":param groupname: Name of group being added\n\n" + ":param kwargs: Keyword arguments ro customize the operation\n\n" + "* groupmod can be customized further with keyword arguments:\n" + " * ``gid``: The GID of the group\n"); + +static PyObject *py_sss_groupadd(PySssLocalObject *self, + PyObject *args, + PyObject *kwds) +{ + struct tools_ctx *tctx = NULL; + char *groupname; + unsigned long gid = 0; + int ret; + const char * const kwlist[] = { "groupname", "gid", NULL }; + + /* parse arguments */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, + discard_const_p(char, "s|k"), + discard_const_p(char *, kwlist), + &groupname, + &gid)) { + goto fail; + } + + tctx = init_ctx(self->mem_ctx, self); + if (!tctx) { + PyErr_NoMemory(); + return NULL; + } + + tctx->octx->name = groupname; + tctx->octx->gid = gid; + + /* Add the group within a transaction */ + start_transaction(tctx); + if (tctx->error != EOK) { + PyErr_SetSssError(tctx->error); + goto fail; + } + + /* groupadd */ + ret = groupadd(tctx, self->ev, + self->sysdb, tctx->handle, tctx->octx); + if (ret != EOK) { + tctx->error = ret; + + /* cancel transaction */ + talloc_zfree(tctx->handle); + PyErr_SetSssError(tctx->error); + goto fail; + } + + end_transaction(tctx); + if (tctx->error) { + PyErr_SetSssError(tctx->error); + goto fail; + } + + talloc_zfree(tctx); + Py_RETURN_NONE; + +fail: + talloc_zfree(tctx); + return NULL; +} + +/* + * Delete a group + */ +PyDoc_STRVAR(py_sss_groupdel__doc__, + "Remove a group.\n\n" + ":param groupname: Name of group being removed\n"); + +static PyObject *py_sss_groupdel(PySssLocalObject *self, + PyObject *args, + PyObject *kwds) +{ + struct tools_ctx *tctx = NULL; + char *groupname = NULL; + int ret; + + if(!PyArg_ParseTuple(args, discard_const_p(char, "s"), &groupname)) { + goto fail; + } + + tctx = init_ctx(self->mem_ctx, self); + if (!tctx) { + PyErr_NoMemory(); + return NULL; + } + + tctx->octx->name = groupname; + + /* Remove the group within a transaction */ + start_transaction(tctx); + if (tctx->error != EOK) { + PyErr_SetSssError(tctx->error); + goto fail; + } + + /* groupdel */ + ret = groupdel(tctx, self->ev, + self->sysdb, tctx->handle, tctx->octx); + if (ret != EOK) { + tctx->error = ret; + + /* cancel transaction */ + talloc_zfree(tctx->handle); + PyErr_SetSssError(tctx->error); + goto fail; + } + + end_transaction(tctx); + if (tctx->error) { + PyErr_SetSssError(tctx->error); + goto fail; + } + + talloc_zfree(tctx); + Py_RETURN_NONE; + +fail: + talloc_zfree(tctx); + return NULL; +} + +/* + * Modify a group + */ +PyDoc_STRVAR(py_sss_groupmod__doc__, +"Modify a group.\n\n" +":param groupname: Name of group being modified\n\n" +":param kwargs: Keyword arguments ro customize the operation\n\n" +"* groupmod can be customized further with keyword arguments:\n" +" * ``gid``: The GID of the group\n\n" +" * ``addgroups``: Groups to add the group to\n\n" +" * ``rmgroups``: Groups to remove the group from\n\n"); + +static PyObject *py_sss_groupmod(PySssLocalObject *self, + PyObject *args, + PyObject *kwds) +{ + struct tools_ctx *tctx = NULL; + int ret; + PyObject *py_addgroups = Py_None; + PyObject *py_rmgroups = Py_None; + unsigned long gid = 0; + char *groupname = NULL; + const char * const kwlist[] = { "groupname", "gid", "addgroups", + "rmgroups", NULL }; + + /* parse arguments */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, + discard_const_p(char, "s|kO!O!"), + discard_const_p(char *, kwlist), + &groupname, + &gid, + &PyList_Type, + &py_addgroups, + &PyList_Type, + &py_rmgroups)) { + goto fail; + } + + tctx = init_ctx(self->mem_ctx, self); + if (!tctx) { + PyErr_NoMemory(); + return NULL; + } + + if (py_addgroups != Py_None) { + tctx->octx->addgroups = PyList_AsStringList(tctx, + py_addgroups, + "addgroups"); + if (!tctx->octx->addgroups) { + return NULL; + } + } + + if (py_rmgroups != Py_None) { + tctx->octx->rmgroups = PyList_AsStringList(tctx, + py_rmgroups, + "rmgroups"); + if (!tctx->octx->rmgroups) { + return NULL; + } + } + + tctx->octx->name = groupname; + tctx->octx->gid = gid; + + /* Modify the group within a transaction */ + start_transaction(tctx); + if (tctx->error != EOK) { + PyErr_SetSssError(tctx->error); + goto fail; + } + + /* groupmod */ + ret = groupmod(tctx, self->ev, + self->sysdb, tctx->handle, tctx->octx); + if (ret != EOK) { + tctx->error = ret; + + /* cancel transaction */ + talloc_zfree(tctx->handle); + PyErr_SetSssError(tctx->error); + goto fail; + } + + end_transaction(tctx); + if (tctx->error) { + PyErr_SetSssError(tctx->error); + goto fail; + } + + talloc_zfree(tctx); + Py_RETURN_NONE; + +fail: + talloc_zfree(tctx); + 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; + char *confdb_path; + 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; + + self->ev = tevent_context_init(mem_ctx); + if (self->ev == NULL) { + talloc_free(mem_ctx); + PyErr_SetSssErrorWithMessage(EIO, "Cannot create event context"); + return NULL; + } + + confdb_path = talloc_asprintf(self->mem_ctx, "%s/%s", DB_PATH, CONFDB_FILE); + if (confdb_path == NULL) { + talloc_free(mem_ctx); + PyErr_NoMemory(); + return NULL; + } + + /* Connect to the conf db */ + ret = confdb_init(self->mem_ctx, &self->confdb, confdb_path); + if (ret != EOK) { + talloc_free(mem_ctx); + PyErr_SetSssErrorWithMessage(ret, + "Could not initialize connection to the confdb\n"); + return NULL; + } + + ret = confdb_get_domain(self->confdb, "local", &self->local); + if (ret != EOK) { + talloc_free(mem_ctx); + PyErr_SetSssErrorWithMessage(ret, "Cannot get local domain"); + return NULL; + } + + /* open 'local' sysdb at default path */ + ret = sysdb_domain_init(self->mem_ctx, self->ev, self->local, DB_PATH, &self->sysdb); + if (ret != EOK) { + talloc_free(mem_ctx); + PyErr_SetSssErrorWithMessage(ret, + "Could not initialize connection to the sysdb\n"); + 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, py_sss_useradd__doc__ + }, + { "userdel", (PyCFunction) py_sss_userdel, + METH_KEYWORDS, py_sss_userdel__doc__ + }, + { "usermod", (PyCFunction) py_sss_usermod, + METH_KEYWORDS, py_sss_usermod__doc__ + }, + { "groupadd", (PyCFunction) py_sss_groupadd, + METH_KEYWORDS, py_sss_groupadd__doc__ + }, + { "groupdel", (PyCFunction) py_sss_groupdel, + METH_KEYWORDS, py_sss_groupdel__doc__ + }, + { "groupmod", (PyCFunction) py_sss_groupmod, + METH_KEYWORDS, py_sss_groupmod__doc__ + }, + {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(discard_const_p(char, "pysss"), module_methods); + if (m == NULL) + return; + + Py_INCREF(&pysss_local_type); + PyModule_AddObject(m, discard_const_p(char, "local"), (PyObject *)&pysss_local_type); +} + |