This is an automated email from the git hooks/post-receive script.
firstyear pushed a commit to branch 389-ds-base-1.3.7
in repository 389-ds-base.
commit 06582972cdaf65e1dcebc12a37c26701f68ef9fa
Author: Nathan Kinder <nkinder(a)redhat.com>
Date: Mon Sep 23 19:49:23 2013 -0700
Ticket 49363 - Merge lib389, all lib389 history in single patch
Bug Description: The issue is that we have a split: we have tests in
389-ds-base/dirsrvtests that are often version dependent. They relate
to features of the server, or issues in specific versions of the server
that may not exist in older versions. Today we kind of stradle the line
of "it's a bit of both". We have tests in 389-ds-base that depend on
versions of lib389 - but lib389 moves quickly and has little ability to
distinguish 389-ds-base versions.
Fix Description: Merge lib389 source code and update related build components.
https://pagure.io/389-ds-base/issue/49363
Author: wibrown
Review by: wibrown (thanks everyone!)
---
Makefile.am | 17 +-
rpm/389-ds-base.spec.in | 94 +-
src/lib389/.coveragerc | 2 +
src/lib389/.gitignore | 10 +
src/lib389/LICENSE | 675 +++++
src/lib389/MANIFEST.in | 2 +
src/lib389/Makefile | 33 +
src/lib389/README.md | 26 +
src/lib389/VERSION | 1 +
src/lib389/bug_harness.py | 107 +
src/lib389/cli/dsconf | 124 +
src/lib389/cli/dscreate | 84 +
src/lib389/cli/dsctl | 101 +
src/lib389/cli/dsidm | 116 +
src/lib389/doc/Makefile | 230 ++
src/lib389/doc/source/accesscontrol.rst | 6 +
src/lib389/doc/source/aci.rst | 66 +
src/lib389/doc/source/agreement.rst | 27 +
src/lib389/doc/source/backend.rst | 49 +
src/lib389/doc/source/changelog.rst | 22 +
src/lib389/doc/source/conf.py | 335 ++
src/lib389/doc/source/config.rst | 47 +
src/lib389/doc/source/databases.rst | 7 +
src/lib389/doc/source/dirsrv_log.rst | 30 +
src/lib389/doc/source/domain.rst | 21 +
src/lib389/doc/source/dseldif.rst | 30 +
src/lib389/doc/source/group.rst | 43 +
src/lib389/doc/source/guidelines.rst | 599 ++++
src/lib389/doc/source/identitymanagement.rst | 10 +
src/lib389/doc/source/index.rst | 51 +
src/lib389/doc/source/indexes.rst | 42 +
src/lib389/doc/source/ldclt.rst | 42 +
src/lib389/doc/source/mappingtree.rst | 31 +
src/lib389/doc/source/monitor.rst | 19 +
src/lib389/doc/source/need_to_be_triaged.rst | 20 +
src/lib389/doc/source/organisationalunit.rst | 41 +
src/lib389/doc/source/passwd.rst | 33 +
src/lib389/doc/source/paths.rst | 42 +
src/lib389/doc/source/plugin.rst | 35 +
src/lib389/doc/source/replica.rst | 59 +
src/lib389/doc/source/replication.rst | 9 +
src/lib389/doc/source/repltools.rst | 43 +
src/lib389/doc/source/rootdse.rst | 25 +
src/lib389/doc/source/schema.rst | 62 +
src/lib389/doc/source/services.rst | 40 +
src/lib389/doc/source/task.rst | 59 +
src/lib389/doc/source/user.rst | 50 +
src/lib389/doc/source/utils.rst | 23 +
src/lib389/dsadmin.pylintrc | 236 ++
src/lib389/lib389/__init__.py | 3189 ++++++++++++++++++++
src/lib389/lib389/_constants.py | 309 ++
src/lib389/lib389/_controls.py | 137 +
src/lib389/lib389/_entry.py | 631 ++++
src/lib389/lib389/_ldifconn.py | 53 +
src/lib389/lib389/_mapped_object.py | 840 ++++++
src/lib389/lib389/_replication.py | 202 ++
src/lib389/lib389/aci.py | 240 ++
src/lib389/lib389/agreement.py | 756 +++++
src/lib389/lib389/backend.py | 576 ++++
src/lib389/lib389/changelog.py | 153 +
src/lib389/lib389/cli_base/__init__.py | 170 ++
src/lib389/lib389/cli_base/dsrc.py | 118 +
src/lib389/lib389/cli_conf/__init__.py | 8 +
src/lib389/lib389/cli_conf/backend.py | 76 +
src/lib389/lib389/cli_conf/health.py | 61 +
src/lib389/lib389/cli_conf/plugin.py | 124 +
src/lib389/lib389/cli_conf/plugins/__init__.py | 0
src/lib389/lib389/cli_conf/plugins/memberof.py | 276 ++
src/lib389/lib389/cli_conf/plugins/referint.py | 197 ++
src/lib389/lib389/cli_conf/plugins/rootdn_ac.py | 229 ++
src/lib389/lib389/cli_conf/plugins/usn.py | 56 +
src/lib389/lib389/cli_conf/plugins/whoami.py | 16 +
src/lib389/lib389/cli_conf/schema.py | 53 +
src/lib389/lib389/cli_ctl/__init__.py | 8 +
src/lib389/lib389/cli_ctl/dbtasks.py | 56 +
src/lib389/lib389/cli_ctl/instance.py | 133 +
src/lib389/lib389/cli_idm/__init__.py | 114 +
src/lib389/lib389/cli_idm/account.py | 64 +
src/lib389/lib389/cli_idm/group.py | 76 +
src/lib389/lib389/cli_idm/initialise.py | 21 +
src/lib389/lib389/cli_idm/organisationalunit.py | 76 +
src/lib389/lib389/cli_idm/posixgroup.py | 77 +
src/lib389/lib389/cli_idm/user.py | 76 +
src/lib389/lib389/clitools/__init__.py | 79 +
src/lib389/lib389/clitools/ds_aci_lint | 41 +
src/lib389/lib389/clitools/ds_backend_getattr | 42 +
src/lib389/lib389/clitools/ds_backend_list | 39 +
src/lib389/lib389/clitools/ds_backend_setattr | 43 +
src/lib389/lib389/clitools/ds_krb_create_keytab | 40 +
src/lib389/lib389/clitools/ds_krb_create_principal | 38 +
src/lib389/lib389/clitools/ds_krb_create_realm | 34 +
src/lib389/lib389/clitools/ds_krb_destroy_realm | 34 +
src/lib389/lib389/clitools/ds_monitor_backend | 39 +
src/lib389/lib389/clitools/ds_monitor_server | 35 +
.../lib389/clitools/ds_schema_attributetype_list | 33 +
.../lib389/clitools/ds_schema_attributetype_query | 46 +
src/lib389/lib389/clitools/ds_setup | 78 +
src/lib389/lib389/config.py | 353 +++
src/lib389/lib389/configurations/__init__.py | 25 +
src/lib389/lib389/configurations/config.py | 44 +
.../lib389/configurations/config_001003006.py | 139 +
src/lib389/lib389/configurations/sample.py | 21 +
src/lib389/lib389/dbgen.py | 191 ++
src/lib389/lib389/dirsrv_log.py | 271 ++
src/lib389/lib389/ds_instance.py | 97 +
src/lib389/lib389/dseldif.py | 101 +
src/lib389/lib389/exceptions.py | 43 +
src/lib389/lib389/extended_operations.py | 67 +
src/lib389/lib389/idm/__init__.py | 0
src/lib389/lib389/idm/account.py | 50 +
src/lib389/lib389/idm/domain.py | 21 +
src/lib389/lib389/idm/group.py | 89 +
src/lib389/lib389/idm/organisationalunit.py | 38 +
src/lib389/lib389/idm/posixgroup.py | 52 +
src/lib389/lib389/idm/services.py | 36 +
src/lib389/lib389/idm/user.py | 85 +
src/lib389/lib389/index.py | 124 +
src/lib389/lib389/instance/__init__.py | 7 +
src/lib389/lib389/instance/options.py | 301 ++
src/lib389/lib389/instance/remove.py | 54 +
src/lib389/lib389/instance/setup.py | 454 +++
src/lib389/lib389/ldclt.py | 130 +
src/lib389/lib389/lint.py | 131 +
src/lib389/lib389/mappingTree.py | 420 +++
src/lib389/lib389/mit_krb5.py | 226 ++
src/lib389/lib389/monitor.py | 94 +
src/lib389/lib389/ns-slapd.valgrind | 29 +
src/lib389/lib389/nss_ssl.py | 371 +++
src/lib389/lib389/passwd.py | 51 +
src/lib389/lib389/paths.py | 186 ++
src/lib389/lib389/plugins.py | 813 +++++
src/lib389/lib389/properties.py | 498 +++
src/lib389/lib389/referral.py | 35 +
src/lib389/lib389/replica.py | 1452 +++++++++
src/lib389/lib389/repltools.py | 298 ++
src/lib389/lib389/rootdse.py | 47 +
src/lib389/lib389/sasl.py | 36 +
src/lib389/lib389/schema.py | 236 ++
src/lib389/lib389/suffix.py | 98 +
src/lib389/lib389/tasks.py | 1195 ++++++++
src/lib389/lib389/tests/__init__.py | 7 +
src/lib389/lib389/tests/aci_test.py | 122 +
src/lib389/lib389/tests/agreement_test.py | 362 +++
src/lib389/lib389/tests/backend_test.py | 282 ++
src/lib389/lib389/tests/cli/__init__.py | 104 +
src/lib389/lib389/tests/cli/adm_instance_test.py | 31 +
src/lib389/lib389/tests/cli/conf_backend_test.py | 52 +
src/lib389/lib389/tests/cli/conf_plugin_test.py | 53 +
.../lib389/tests/cli/conf_plugins/__init__.py | 0
.../lib389/tests/cli/conf_plugins/memberof_test.py | 490 +++
.../lib389/tests/cli/conf_plugins/referint_test.py | 119 +
.../tests/cli/conf_plugins/rootdn_ac_test.py | 281 ++
.../lib389/tests/cli/conf_plugins/usn_test.py | 53 +
src/lib389/lib389/tests/cli/ctl_dbtasks_test.py | 71 +
src/lib389/lib389/tests/cli/dsrc_test.py | 180 ++
src/lib389/lib389/tests/config.py | 40 +
src/lib389/lib389/tests/config_compare_test.py | 56 +
src/lib389/lib389/tests/config_test.py | 87 +
src/lib389/lib389/tests/configurations/__init__.py | 0
.../tests/configurations/config_001003006_test.py | 98 +
src/lib389/lib389/tests/conftest.py | 121 +
src/lib389/lib389/tests/dereference_test.py | 99 +
src/lib389/lib389/tests/dirsrv_log_test.py | 123 +
src/lib389/lib389/tests/dirsrv_test.py | 215 ++
src/lib389/lib389/tests/dsadmin_basic_test.py | 126 +
.../lib389/tests/dsadmin_create_remove_test.py | 97 +
src/lib389/lib389/tests/dsadmin_test.py | 263 ++
src/lib389/lib389/tests/dseldif_test.py | 130 +
src/lib389/lib389/tests/effective_rights_test.py | 93 +
src/lib389/lib389/tests/entry_test.py | 98 +
src/lib389/lib389/tests/healthcheck_test.py | 61 +
src/lib389/lib389/tests/idm/__init__.py | 0
src/lib389/lib389/tests/idm/account_test.py | 70 +
src/lib389/lib389/tests/idm/services_test.py | 55 +
src/lib389/lib389/tests/idm/user_and_group_test.py | 93 +
.../lib389/tests/idm/user_compare_i2_test.py | 67 +
.../lib389/tests/idm/user_compare_m2Repl_test.py | 83 +
.../lib389/tests/idm/user_compare_st_test.py | 92 +
src/lib389/lib389/tests/index_test.py | 69 +
src/lib389/lib389/tests/instance/__init__.py | 8 +
src/lib389/lib389/tests/instance/setup_test.py | 121 +
src/lib389/lib389/tests/krb5_create_test.py | 147 +
src/lib389/lib389/tests/ldclt_test.py | 64 +
src/lib389/lib389/tests/mapped_object_test.py | 23 +
src/lib389/lib389/tests/mappingTreeLegacy_test.py | 271 ++
src/lib389/lib389/tests/mappingtree_test.py | 64 +
src/lib389/lib389/tests/nss_ssl_test.py | 74 +
src/lib389/lib389/tests/paths_test.py | 36 +
src/lib389/lib389/tests/plugin_test.py | 137 +
src/lib389/lib389/tests/plugins/__init__.py | 0
src/lib389/lib389/tests/plugins/memberof_test.py | 354 +++
src/lib389/lib389/tests/plugins/referint_test.py | 83 +
src/lib389/lib389/tests/plugins/usn_test.py | 240 ++
src/lib389/lib389/tests/plugins/utils.py | 124 +
src/lib389/lib389/tests/referral_test.py | 36 +
src/lib389/lib389/tests/replicaLegacy_test.py | 457 +++
src/lib389/lib389/tests/replica_test.py | 375 +++
src/lib389/lib389/tests/schema_test.py | 72 +
src/lib389/lib389/tests/suffix_test.py | 105 +
src/lib389/lib389/tests/test_module_proxy.py | 79 +
src/lib389/lib389/tests/tls_external_test.py | 82 +
src/lib389/lib389/tests/utils_test.py | 139 +
src/lib389/lib389/tools.py | 1008 +++++++
src/lib389/lib389/topologies.py | 382 +++
src/lib389/lib389/utils.py | 873 ++++++
src/lib389/python-lib389.spec | 239 ++
src/lib389/requirements.txt | 9 +
src/lib389/setup.cfg | 3 +
src/lib389/setup.py | 66 +
src/lib389/tox.ini | 6 +
210 files changed, 32747 insertions(+), 50 deletions(-)
diff --git a/Makefile.am b/Makefile.am
index 8834a78..039a1d0 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -12,6 +12,7 @@ QUOTE := $(NULLSTRING)"# a double quote"
# First, we setup the definitions from configure.ac
#
+PYTHON := python3
BUILDNUM := $(shell perl $(srcdir)/buildnum.pl)
NQBUILDNUM := $(subst \,,$(subst $(QUOTE),,$(BUILDNUM)))
DEBUG_DEFINES = @debug_defs@
@@ -582,7 +583,9 @@ dist_noinst_DATA = \
$(srcdir)/setup.py.in \
$(srcdir)/wrappers/*.in \
$(srcdir)/wrappers/systemd.template.sysconfig \
- $(srcdir)/dirsrvtests
+ $(srcdir)/dirsrvtests \
+ $(srcdir)/src/lib389/setup.py \
+ $(srcdir)/src/lib389
if ENABLE_PERL
dist_noinst_DATA += \
@@ -2219,11 +2222,17 @@ git-archive:
# Python test tests
# How will we update this to python 3?
-tests: setup.py.in
- python setup.py build
+lib389: src/lib389/setup.py
+ cd $(srcdir)/src/lib389; $(PYTHON) setup.py build
+
+lib389-install: lib389
+ cd $(srcdir)/src/lib389; $(PYTHON) setup.py install --skip-build
+
+tests: setup.py.in lib389
+ $(PYTHON) setup.py build
tests-install: tests
- python setup.py install
+ $(PYTHON) setup.py install
# RPM-related tasks
diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in
index 30a1d7d..b4c863a 100644
--- a/rpm/389-ds-base.spec.in
+++ b/rpm/389-ds-base.spec.in
@@ -85,6 +85,10 @@ BuildRequires: doxygen
# For tests!
BuildRequires: libcmocka-devel
BuildRequires: libevent-devel
+# For lib389 and related components.
+BuildRequires: python%{python3_pkgversion}
+BuildRequires: python%{python3_pkgversion}-devel
+BuildRequires: python%{python3_pkgversion}-setuptools
%if %{use_tcmalloc}
BuildRequires: gperftools-devel
@@ -206,46 +210,35 @@ Obsoletes: %{name} <= 1.3.5.4
SNMP Agent for the 389 Directory Server base package.
-
-
-%package -n python2-%{srcname}-tests
-Summary: The lib389 Continuous Integration Tests
-Group: Development/Libraries
+%package -n python%{python3_pkgversion}-lib389
+Summary: A library for accessing, testing, and configuring the 389 Directory Server
BuildArch: noarch
-Requires: python2-lib389
-%if 0%{?rhel}
-Requires: python
-BuildRequires: python
-BuildRequires: python-devel
-BuildRequires: python-setuptools
-%else
-Requires: python2
-BuildRequires: python2
-BuildRequires: python2-devel
-BuildRequires: python2-setuptools
-%endif
-
-%description -n python2-%{srcname}-tests
-The lib389 CI tests that can be run against the Directory Server.
-
-
+Group: Development/Libraries
+Requires: krb5-workstation
+Requires: krb5-server
+Requires: openssl
+Requires: iproute
+Requires: python%{python3_pkgversion}
+Requires: python%{python3_pkgversion}-pytest
+Requires: python%{python3_pkgversion}-pyldap
+Requires: python%{python3_pkgversion}-six
+Requires: python%{python3_pkgversion}-pyasn1
+Requires: python%{python3_pkgversion}-pyasn1-modules
+Requires: python%{python3_pkgversion}-dateutil
+%{?python_provide:%python_provide python%{python3_pkgversion}-lib389}
+%description -n python%{python3_pkgversion}-lib389
+This module contains tools and libraries for accessing, testing,
+ and configuring the 389 Directory Server.
-# Can't build on EL7! Python3 tooling is too broken :(
-# We have to use >= 8, because <= 7 doesn't work ....
-%if 0%{?rhel} >= 8 || 0%{?fedora}
%package -n python%{python3_pkgversion}-%{srcname}-tests
Summary: The lib389 Continuous Integration Tests
Group: Development/Libraries
BuildArch: noarch
Requires: python%{python3_pkgversion}
Requires: python%{python3_pkgversion}-lib389
-BuildRequires: python%{python3_pkgversion}
-BuildRequires: python%{python3_pkgversion}-devel
-BuildRequires: python%{python3_pkgversion}-setuptools
%description -n python%{python3_pkgversion}-%{srcname}-tests
The lib389 CI tests that can be run against the Directory Server.
-%endif
%prep
%setup -q -n %{name}-%{version}%{?prerel}
@@ -277,20 +270,22 @@ autoreconf -fiv
$NSSARGS $TCMALLOC_FLAGS $ASAN_FLAGS \
--enable-cmocka
+make setup.py
+
+# lib389
+pushd ./src/lib389
+%py3_build
+popd
+
+# tests
+%py3_build
+
+
# Generate symbolic info for debuggers
export XCFLAGS=$RPM_OPT_FLAGS
make %{?_smp_mflags}
-make setup.py
-
-%if 0%{?rhel} >= 8 || 0%{?fedora}
-%py2_build
-%py3_build
-%else
-%{__python} setup.py build
-%endif
-
%install
rm -rf $RPM_BUILD_ROOT
@@ -300,12 +295,13 @@ make DESTDIR="$RPM_BUILD_ROOT" install
# Copy in our docs from doxygen.
cp -r %{_builddir}/%{name}-%{version}%{?prerel}/man/man3 $RPM_BUILD_ROOT/%{_mandir}/man3
-%if 0%{?rhel} >= 8 || 0%{?fedora}
-%py2_install
+# lib389
+pushd src/lib389
+%py3_install
+popd
+
+# tests
%py3_install
-%else
-%{__python} setup.py install -O1 --skip-build --root %{buildroot}
-%endif
mkdir -p $RPM_BUILD_ROOT/var/log/%{pkgname}
mkdir -p $RPM_BUILD_ROOT/var/lib/%{pkgname}
@@ -535,16 +531,22 @@ fi
%{_mandir}/man1/ldap-agent.1.gz
%{_unitdir}/%{pkgname}-snmp.service
-%files -n python2-%{srcname}-tests
+%if 0%{?rhel} >= 8 || 0%{?fedora}
+%files -n python%{python3_pkgversion}-lib389
%defattr(-,root,root,-)
%doc LICENSE LICENSE.GPLv3+
-%{python2_sitelib}/*
+%{_sbindir}/dsconf
+%{_sbindir}/dscreate
+%{_sbindir}/dsctl
+%{_sbindir}/dsidm
+%{python3_sitelib}/lib389*
+%endif
%if 0%{?rhel} >= 8 || 0%{?fedora}
%files -n python%{python3_pkgversion}-%{srcname}-tests
%defattr(-,root,root,-)
%doc LICENSE LICENSE.GPLv3+
-%{python3_sitelib}/*
+%{python3_sitelib}/dirsrvtests*
%endif
%changelog
diff --git a/src/lib389/.coveragerc b/src/lib389/.coveragerc
new file mode 100644
index 0000000..36c6a13
--- /dev/null
+++ b/src/lib389/.coveragerc
@@ -0,0 +1,2 @@
+[run]
+omit = lib389/tests/*
diff --git a/src/lib389/.gitignore b/src/lib389/.gitignore
new file mode 100644
index 0000000..ae200f7
--- /dev/null
+++ b/src/lib389/.gitignore
@@ -0,0 +1,10 @@
+# Log directory #
+###################
+logs/
+build/
+
+*.pyc
+*.patch
+.cache
+.tox
+*.egg-info
diff --git a/src/lib389/LICENSE b/src/lib389/LICENSE
new file mode 100644
index 0000000..10926e8
--- /dev/null
+++ b/src/lib389/LICENSE
@@ -0,0 +1,675 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <
http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means
any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+
diff --git a/src/lib389/MANIFEST.in b/src/lib389/MANIFEST.in
new file mode 100644
index 0000000..b2a080a
--- /dev/null
+++ b/src/lib389/MANIFEST.in
@@ -0,0 +1,2 @@
+include README
+include VERSION
diff --git a/src/lib389/Makefile b/src/lib389/Makefile
new file mode 100644
index 0000000..5e4332c
--- /dev/null
+++ b/src/lib389/Makefile
@@ -0,0 +1,33 @@
+.PHONY: build install test rpms srpms
+
+LIB389_VERS ?= $(shell cat ./VERSION | head -n 1)
+PYTHON ?= /usr/bin/python
+RPMBUILD ?= $(shell pwd)/rpmbuild
+
+all: build
+
+build:
+ $(PYTHON) setup.py build
+
+install:
+ $(PYTHON) setup.py install --skip-build --force --root=/
+
+rpmbuild-prep:
+ mkdir -p ./dist/
+ mkdir -p $(RPMBUILD)/SOURCES
+ mkdir -p $(RPMBUILD)/SPECS
+ git archive --prefix=python-lib389-$(LIB389_VERS)-1/ HEAD | bzip2 >
./dist/python-lib389-$(LIB389_VERS)-1.tar.bz2
+ cp ./dist/python-lib389-$(LIB389_VERS)-1.tar.bz2 $(RPMBUILD)/SOURCES/
+
+srpm: rpmbuild-prep
+ rpmbuild --define "_topdir $(RPMBUILD)" -bs python-lib389.spec
+
+rpm: rpmbuild-prep
+ rpmbuild --define "_topdir $(RPMBUILD)" -bb python-lib389.spec
+
+pep8:
+ pep8 --max-line-length=160 ./lib389
+# Needs to generally be run as sudo
+test:
+ py.test -s ./lib389/tests
+
diff --git a/src/lib389/README.md b/src/lib389/README.md
new file mode 100644
index 0000000..95d8df0
--- /dev/null
+++ b/src/lib389/README.md
@@ -0,0 +1,26 @@
+Lib389
+======
+
+This repository contains tools and libraries for testing 389 Directory Server.
+
+It can be used in place, or it can be built and installed with the provided
+Makefile.
+
+To compile the python code:
+
+ make build
+
+To install the python code:
+
+ sudo make install
+
+To build an rpm or srpm
+
+ make [s]rpm
+
+To test the lib389 system (Requires an installation of 389-ds-base)
+
+ sudo make test
+
+
+
diff --git a/src/lib389/VERSION b/src/lib389/VERSION
new file mode 100644
index 0000000..ee90284
--- /dev/null
+++ b/src/lib389/VERSION
@@ -0,0 +1 @@
+1.0.4
diff --git a/src/lib389/bug_harness.py b/src/lib389/bug_harness.py
new file mode 100644
index 0000000..ba61344
--- /dev/null
+++ b/src/lib389/bug_harness.py
@@ -0,0 +1,107 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from bug_harness import DSAdminHarness as DSAdmin
+from dsadmin.tools import DSAdminTools
+from lib389.properties import *
+"""
+ An harness for bug replication.
+
+"""
+import os
+
+REPLBINDDN = ''
+REPLBINDPW = ''
+
+
+@static_var("REPLICAID", 1)
+def get_next_replicaid(replica_id=None, replica_type=None):
+ if replica_id:
+ REPLICAID = replica_id
+ return REPLICAID
+ # get a default replica_id if it's a MASTER,
+ # or 0 if consumer
+ if replica_type == MASTER_TYPE:
+ REPLICAID += 1
+ return REPLICAID
+
+ return 0
+
+
+class DSAdminHarness(DSAdmin, DSAdminTools):
+ """Harness wrapper around dsadmin.
+
+ Specialize the DSAdmin behavior (No, I don't care about Liskov ;))
+ """
+ def setupSSL(self, secport, sourcedir=os.environ['SECDIR'], secargs=None):
+ """Bug scripts requires SECDIR."""
+ return DSAdminTools.setupSSL(self, secport, sourcedir, secargs)
+
+ def setupAgreement(self, repoth, args):
+ """Set default replia credentials """
+ args.setdefault('binddn', REPLBINDDN)
+ args.setdefault('bindpw', REPLBINDPW)
+
+ return DSAdmin.createAgreement(self, repoth, args)
+
+ def setupReplica(self, args):
+ """Set default replia credentials """
+ args.setdefault('binddn', REPLBINDDN)
+ args.setdefault('bindpw', REPLBINDPW)
+ # manage a progressive REPLICAID
+ args.setdefault(
+ 'id', get_next_replicaid(args.get('id'),
args.get('type')))
+ return DSAdmin.setupReplica(self, args)
+
+ def setupBindDN(self, binddn=REPLBINDDN, bindpw=REPLBINDPW):
+ return DSAdmin.setupBindDN(self, binddn, bindpw)
+
+ def setupReplBindDN(self, binddn=REPLBINDDN, bindpw=REPLBINDPW):
+ return self.setupBindDN(binddn, bindpw)
+
+ def setupBackend(self, suffix, binddn=None, bindpw=None, urls=None,
+ attrvals=None, benamebase=None, verbose=False):
+ """Create a backends using the first available
cn."""
+ # if benamebase is set, try creating without appending
+ if benamebase:
+ benum = 0
+ else:
+ benum = 1
+
+ # figure out what type of be based on args
+ if binddn and bindpw and urls: # its a chaining be
+ benamebase = benamebase or "chaindb"
+ else: # its a ldbm be
+ benamebase = benamebase or "localdb"
+
+ done = False
+ while not done:
+ # if benamebase is set, benum starts at 0
+ # and the first attempt tries to create the
+ # simple benamebase. On failure benum is
+ # incremented and the suffix is appended
+ # to the cn
+ if benum:
+ benamebase_tmp = benamebase + str(benum) # e.g. localdb1
+ else:
+ benamebase_tmp = benamebase
+
+ try:
+ cn = DSAdmin.setupBackend(suffix, binddn, bindpw,
+ urls, attrvals, benamebase, verbose)
+ done = True
+ except ldap.ALREADY_EXISTS:
+ benum += 1
+
+ return cn
+
+ def createInstance(args):
+ # eventually set prefix
+ args.setdefault(SER_DEPLOYED_DIR, os.environ.get('PREFIX', None))
+ args.setdefault('sroot', os.environ.get('SERVER_ROOT', None))
+ DSAdminTools.createInstance(args)
diff --git a/src/lib389/cli/dsconf b/src/lib389/cli/dsconf
new file mode 100755
index 0000000..ea1d056
--- /dev/null
+++ b/src/lib389/cli/dsconf
@@ -0,0 +1,124 @@
+#!/usr/bin/python3
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import argparse
+import logging
+import ldap
+import sys
+
+# This has to happen before we import DirSrv else it tramples our config ... :(
+logging.basicConfig(format='%(message)s')
+
+from lib389 import DirSrv
+from lib389._constants import DN_CONFIG, DN_DM
+from lib389.cli_conf import backend as cli_backend
+from lib389.cli_conf import plugin as cli_plugin
+from lib389.cli_conf import schema as cli_schema
+from lib389.cli_conf import health as cli_health
+from lib389.cli_conf.plugins import memberof as cli_memberof
+from lib389.cli_conf.plugins import usn as cli_usn
+from lib389.cli_conf.plugins import rootdn_ac as cli_rootdn_ac
+from lib389.cli_conf.plugins import whoami as cli_whoami
+from lib389.cli_conf.plugins import referint as cli_referint
+from lib389.cli_base import disconnect_instance, connect_instance
+
+from lib389.cli_base.dsrc import dsrc_to_ldap, dsrc_arg_concat
+
+log = logging.getLogger("dsconf")
+
+if __name__ == '__main__':
+
+ defbase = ldap.get_option(ldap.OPT_DEFBASE)
+
+ parser = argparse.ArgumentParser(allow_abbrev=True)
+ # Build the base ldap options, this keeps in unified.
+
+ # Can we get default options for these from .rc file?
+
+ parser.add_argument('instance',
+ help="The instance name OR the LDAP url to connect to, IE localhost,
ldap://mai.example.com:389",
+ )
+ parser.add_argument('-v', '--verbose',
+ help="Display verbose operation tracing during command execution",
+ action='store_true', default=False
+ )
+ parser.add_argument('-D', '--binddn',
+ help="The account to bind as for executing operations",
+ default=None,
+ )
+ parser.add_argument('-b', '--basedn',
+ help="Basedn (root naming context) of the instance to manage",
+ default=None
+ )
+ parser.add_argument('-Z', '--starttls',
+ help="Connect with StartTLS",
+ default=False, action='store_true'
+ )
+
+ subparsers = parser.add_subparsers(help="resources to act upon")
+
+ cli_backend.create_parser(subparsers)
+ cli_schema.create_parser(subparsers)
+ cli_health.create_parser(subparsers)
+ cli_plugin.create_parser(subparsers)
+ cli_memberof.create_parser(subparsers)
+ cli_usn.create_parser(subparsers)
+ cli_rootdn_ac.create_parser(subparsers)
+ cli_whoami.create_parser(subparsers)
+ cli_referint.create_parser(subparsers)
+
+ args = parser.parse_args()
+
+ if args.verbose:
+ log.setLevel(logging.DEBUG)
+ else:
+ # We aren't verbose, so fix our formatter up to be more minimal ...
+ log.setLevel(logging.INFO)
+
+ log.debug("The 389 Directory Server Configuration Tool")
+ # Leave this comment here: UofA let me take this code with me provided
+ # I gave attribution. -- wibrown
+ log.debug("Inspired by works of: ITS, The University of Adelaide")
+
+ # Now that we have our args, see how they relate with our instance.
+ dsrc_inst = dsrc_to_ldap("~/.dsrc", args.instance,
log.getChild('dsrc'))
+
+ # Now combine this with our arguments
+
+ dsrc_inst = dsrc_arg_concat(args, dsrc_inst)
+
+ log.debug("Called with: %s" % args)
+ log.debug("Instance details: %s" % dsrc_inst)
+
+ # Assert we have a resources to work on.
+ if not hasattr(args, 'func'):
+ log.error("No resource provided to act upon")
+ log.error("USAGE: dsconf [options] <resource> <action> [action
options]")
+ sys.exit(1)
+
+ # Connect
+ # We don't need a basedn, because the config objects derive it properly
+ inst = None
+ if args.verbose:
+ inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose)
+ args.func(inst, None, log, args)
+ else:
+ try:
+ inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose)
+ args.func(inst, None, log, args)
+ except Exception as e:
+ log.debug(e, exc_info=True)
+ log.error("Error: %s" % e)
+ disconnect_instance(inst)
+
+ # Done!
+ log.debug("dsconf is brought to you by the letter H and the number 25.")
+
+
diff --git a/src/lib389/cli/dscreate b/src/lib389/cli/dscreate
new file mode 100755
index 0000000..4743003
--- /dev/null
+++ b/src/lib389/cli/dscreate
@@ -0,0 +1,84 @@
+#!/usr/bin/python3
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import argparse
+import logging
+import sys
+
+# This has to happen before we import DirSrv else it tramples our config ... :(
+logging.basicConfig(format='%(message)s')
+
+from lib389 import DirSrv
+from lib389.cli_ctl import instance as cli_instance
+
+log = logging.getLogger("dscreate")
+
+if __name__ == '__main__':
+
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument('-v', '--verbose',
+ help="Display verbose operation tracing during command execution",
+ action='store_true', default=False
+ )
+
+ subparsers = parser.add_subparsers(help="action")
+
+ fromfile_parser = subparsers.add_parser('fromfile', help="Create an
instance of Directory Server from an inf answer file")
+ fromfile_parser.add_argument('file', help="Inf file to use with prepared
answers. You can generate an example of this with 'dscreate example'")
+ fromfile_parser.add_argument('-n', '--dryrun', help="Validate
system and configurations only. Do not alter the system.",
action='store_true', default=False)
+ fromfile_parser.add_argument('--IsolemnlyswearthatIamuptonogood',
dest="ack",
+ help="""You are here likely here by mistake! You
want setup-ds.pl!
+By setting this value you acknowledge and take responsibility for the fact this command
is UNTESTED and NOT READY. You are ON YOUR OWN!
+""",
+ action='store_true', default=False)
+ fromfile_parser.add_argument('-c', '--containerised',
help="Indicate to the installer that this is running in a container. Used to disable
systemd native components, even if they are installed.", action='store_true',
default=False)
+ fromfile_parser.set_defaults(func=cli_instance.instance_create)
+
+ example_parser = subparsers.add_parser('example', help="Display an
example ini answer file, with comments")
+ example_parser.set_defaults(func=cli_instance.instance_example)
+
+ args = parser.parse_args()
+
+ if args.verbose:
+ log.setLevel(logging.DEBUG)
+ else:
+ log.setLevel(logging.INFO)
+
+ log.debug("The 389 Directory Server Creation Tool")
+ # Leave this comment here: UofA let me take this code with me provided
+ # I gave attribution. -- wibrown
+ log.debug("Inspired by works of: ITS, The University of Adelaide")
+
+ log.debug("Called with: %s", args)
+
+ inst = DirSrv(verbose=args.verbose)
+
+ result = False
+
+ if args.verbose:
+ result = args.func(inst, log, args)
+ else:
+ try:
+ result = args.func(inst, log, args)
+ except Exception as e:
+ log.debug(e, exc_info=True)
+ log.error("Error: %s" % str(e))
+
+ if result is True:
+ log.info('FINISH: Command succeeded')
+ elif result is False:
+ log.info('FAIL: Command failed. See output for details.')
+
+ # Done!
+ log.debug("dscreate is brought to you by the letter S and the number 22.")
+
+ if result is False:
+ sys.exit(1)
diff --git a/src/lib389/cli/dsctl b/src/lib389/cli/dsctl
new file mode 100755
index 0000000..8aa4f4b
--- /dev/null
+++ b/src/lib389/cli/dsctl
@@ -0,0 +1,101 @@
+#!/usr/bin/python3
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import argparse
+import logging
+import sys
+
+# This has to happen before we import DirSrv else it tramples our config ... :(
+logging.basicConfig(format='%(message)s')
+
+from lib389.cli_base import _get_arg
+from lib389 import DirSrv
+from lib389.cli_ctl import instance as cli_instance
+from lib389.cli_ctl import dbtasks as cli_dbtasks
+from lib389.cli_base import disconnect_instance
+
+log = logging.getLogger("dsctl")
+
+if __name__ == '__main__':
+
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument('-v', '--verbose',
+ help="Display verbose operation tracing during command execution",
+ action='store_true', default=False
+ )
+ parser.add_argument('instance',
+ help="The name of the instance to act upon",
+ )
+
+ subparsers = parser.add_subparsers(help="action")
+
+ # We stack our needed options in via submodules.
+
+ cli_instance.create_parser(subparsers)
+ cli_dbtasks.create_parser(subparsers)
+
+ # Then we tell it to execute.
+
+ args = parser.parse_args()
+
+ if args.verbose:
+ log.setLevel(logging.DEBUG)
+ else:
+ log.setLevel(logging.INFO)
+
+ log.debug("The 389 Directory Server Administration Tool")
+ # Leave this comment here: UofA let me take this code with me provided
+ # I gave attribution. -- wibrown
+ log.debug("Inspired by works of: ITS, The University of Adelaide")
+
+ log.debug("Called with: %s", args)
+
+ # Assert we have a resources to work on.
+ if not hasattr(args, 'func'):
+ log.error("No action provided")
+ log.error("USAGE: dsadm [options] <resource> <action> [action
options]")
+ sys.exit(1)
+
+ # Connect
+ # inst = None
+ inst = DirSrv(verbose=args.verbose)
+
+ result = True
+
+ # Allocate the instance based on name
+ insts = inst.list(serverid=args.instance)
+ if len(insts) != 1:
+ raise ValueError("No such instance %s" % args.instance)
+
+ inst.allocate(insts[0])
+ log.debug('Instance allocated')
+
+ if args.verbose:
+ result = args.func(inst, log, args)
+ else:
+ try:
+ result = args.func(inst, log, args)
+ except Exception as e:
+ log.debug(e, exc_info=True)
+ log.error("Error: %s" % str(e))
+ disconnect_instance(inst)
+
+ if result is True:
+ log.info('FINISH: Command succeeded')
+ elif result is False:
+ log.info('FAIL: Command failed. See output for details.')
+
+ # Done!
+ log.debug("dsadm is brought to you by the letter R and the number 27.")
+
+ if result is False:
+ sys.exit(1)
+
diff --git a/src/lib389/cli/dsidm b/src/lib389/cli/dsidm
new file mode 100755
index 0000000..1cfc415
--- /dev/null
+++ b/src/lib389/cli/dsidm
@@ -0,0 +1,116 @@
+#!/usr/bin/python3
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+import argparse
+# import argcomplete
+import logging
+
+# This has to happen before we import DirSrv else it tramples our config ... :(
+logging.basicConfig(format='%(message)s')
+
+from lib389._constants import DN_DM
+from lib389.cli_idm import account as cli_account
+from lib389.cli_idm import initialise as cli_init
+from lib389.cli_idm import organisationalunit as cli_ou
+from lib389.cli_idm import group as cli_group
+from lib389.cli_idm import posixgroup as cli_posixgroup
+from lib389.cli_idm import user as cli_user
+
+from lib389.cli_base import connect_instance, disconnect_instance
+
+from lib389.cli_base.dsrc import dsrc_to_ldap, dsrc_arg_concat
+
+log = logging.getLogger("dsidm")
+
+if __name__ == '__main__':
+
+ defbase = ldap.get_option(ldap.OPT_DEFBASE)
+
+ parser = argparse.ArgumentParser(allow_abbrev=True)
+ # First, add the LDAP options
+
+ parser.add_argument('instance',
+ help="The instance name OR the LDAP url to connect to, IE localhost,
ldap://mai.example.com:389",
+ )
+ parser.add_argument('-b', '--basedn',
+ help="Basedn (root naming context) of the instance to manage",
+ default=None
+ )
+ parser.add_argument('-v', '--verbose',
+ help="Display verbose operation tracing during command execution",
+ action='store_true', default=False
+ )
+ parser.add_argument('-D', '--binddn',
+ help="The account to bind as for executing operations",
+ default=None,
+ )
+ parser.add_argument('-Z', '--starttls',
+ help="Connect with StartTLS",
+ default=False, action='store_true'
+ )
+
+ subparsers = parser.add_subparsers(help="resources to act upon")
+
+ # Call all the other cli modules to register their bits
+
+ cli_account.create_parser(subparsers)
+ cli_group.create_parser(subparsers)
+ cli_init.create_parser(subparsers)
+ cli_ou.create_parser(subparsers)
+ cli_posixgroup.create_parser(subparsers)
+ cli_user.create_parser(subparsers)
+
+ # argcomplete.autocomplete(parser)
+ args = parser.parse_args()
+
+ if args.verbose:
+ log.setLevel(logging.DEBUG)
+ else:
+ log.setLevel(logging.INFO)
+
+ log.debug("The 389 Directory Server Identity Manager")
+ # Leave this comment here: UofA let me take this code with me provided
+ # I gave attribution. -- wibrown
+ log.debug("Inspired by works of: ITS, The University of Adelaide")
+
+ # Now that we have our args, see how they relate with our instance.
+ dsrc_inst = dsrc_to_ldap("~/.dsrc", args.instance,
log.getChild('dsrc'))
+
+ # Now combine this with our arguments
+
+ dsrc_inst = dsrc_arg_concat(args, dsrc_inst)
+
+ log.debug("Called with: %s", args)
+ log.debug("Instance details: %s" % dsrc_inst)
+
+ if dsrc_inst['basedn'] is None:
+ log.error("Must provide a basedn!")
+
+ ldapurl = args.instance
+
+ # Connect
+ inst = None
+ if args.verbose:
+ inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose)
+ args.func(inst, dsrc_inst['basedn'], log, args)
+ else:
+ try:
+ inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose)
+ args.func(inst, dsrc_inst['basedn'], log, args)
+ except Exception as e:
+ log.debug(e, exc_info=True)
+ log.error("Error: %s" % str(e))
+ disconnect_instance(inst)
+
+
+ log.debug("dsidm is brought to you by the letter E and the number 26.")
+
+
diff --git a/src/lib389/doc/Makefile b/src/lib389/doc/Makefile
new file mode 100644
index 0000000..e632556
--- /dev/null
+++ b/src/lib389/doc/Makefile
@@ -0,0 +1,230 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+ $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx
installed, then set the SPHINXBUILD environment variable to point to the full path of the
'$(SPHINXBUILD)' executable. Alternatively you can add the directory with the
executable to your PATH. If you don\'t have Sphinx installed, grab it from
http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+
+.PHONY: help
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " applehelp to make an Apple Help Book"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " epub3 to make an epub3"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or
PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated
items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if
enabled)"
+ @echo " coverage to run coverage check of the documentation (if enabled)"
+ @echo " dummy to check syntax errors of document sources"
+
+.PHONY: clean
+clean:
+ rm -rf $(BUILDDIR)/*
+
+.PHONY: html
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+.PHONY: dirhtml
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+.PHONY: singlehtml
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+.PHONY: pickle
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+.PHONY: json
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+.PHONY: htmlhelp
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+.PHONY: qthelp
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with
the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/lib389.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/lib389.qhc"
+
+.PHONY: applehelp
+applehelp:
+ $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
+ @echo
+ @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
+ @echo "N.B. You won't be able to view it unless you put it in" \
+ "~/Library/Documentation/Help or install it in your application" \
+ "bundle."
+
+.PHONY: devhelp
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/lib389"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/lib389"
+ @echo "# devhelp"
+
+.PHONY: epub
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+.PHONY: epub3
+epub3:
+ $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
+ @echo
+ @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
+
+.PHONY: latex
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+.PHONY: latexpdf
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: latexpdfja
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: text
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+.PHONY: man
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+.PHONY: texinfo
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+.PHONY: info
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+.PHONY: gettext
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+.PHONY: changes
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+.PHONY: linkcheck
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+.PHONY: doctest
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+.PHONY: coverage
+coverage:
+ $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
+ @echo "Testing of coverage in the sources finished, look at the " \
+ "results in $(BUILDDIR)/coverage/python.txt."
+
+.PHONY: xml
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+.PHONY: pseudoxml
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+
+.PHONY: dummy
+dummy:
+ $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
+ @echo
+ @echo "Build finished. Dummy builder generates no files."
diff --git a/src/lib389/doc/source/accesscontrol.rst
b/src/lib389/doc/source/accesscontrol.rst
new file mode 100644
index 0000000..2c5a882
--- /dev/null
+++ b/src/lib389/doc/source/accesscontrol.rst
@@ -0,0 +1,6 @@
+Access Control
+--------------
+
+.. toctree::
+
+ aci.rst
diff --git a/src/lib389/doc/source/aci.rst b/src/lib389/doc/source/aci.rst
new file mode 100644
index 0000000..8c7c0f3
--- /dev/null
+++ b/src/lib389/doc/source/aci.rst
@@ -0,0 +1,66 @@
+ACI
+==========
+
+Usage example
+------------------
+::
+
+ ACI_TARGET = ('(targetfilter ="(ou=groups)")(targetattr
="uniqueMember '
+ '|| member")')
+ ACI_ALLOW = ('(version 3.0; acl "Allow test aci";allow (read, search,
'
+ 'write)')
+ ACI_SUBJECT =
('(userdn="ldap:///dc=example,dc=com??sub?(ou=engineering)" '
+ 'and
userdn="ldap:///dc=example,dc=com??sub?(manager=uid='
+ 'wbrown,ou=managers,dc=example,dc=com) || ldap:///dc=examp'
+ 'le,dc=com??sub?(manager=uid=tbrown,ou=managers,dc=exampl'
+ 'e,dc=com)" );)')
+
+ # Add some entry with ACI
+ group_dn = 'cn=testgroup,{}'.format(DEFAULT_SUFFIX)
+ gentry = Entry(group_dn)
+ gentry.setValues('objectclass', 'top', 'extensibleobject')
+ gentry.setValues('cn', 'testgroup')
+ gentry.setValues('aci', ACI_BODY)
+ standalone.add_s(gentry)
+
+ # Get and parse ACI
+ acis = standalone.aci.list()
+ aci = acis[0]
+
+ assert aci.acidata == {
+ 'allow': [{'values': ['read', 'search',
'write']}],
+ 'target': [], 'targetattr': [{'values':
['uniqueMember', 'member'],
+ 'equal': True}],
+ 'targattrfilters': [],
+ 'deny': [],
+ 'acl': [{'values': ['Allow test aci']}],
+ 'deny_raw_bindrules': [],
+ 'targetattrfilters': [],
+ 'allow_raw_bindrules': [{'values': [(
+ 'userdn="ldap:///dc=example,dc=com??sub?(ou=engineering)"
and'
+ ' userdn="ldap:///dc=example,dc=com??sub?(manager=uid=wbrown,'
+ 'ou=managers,dc=example,dc=com) || ldap:///dc=example,dc=com'
+ '??sub?(manager=uid=tbrown,ou=managers,dc=example,dc=com)"
')]}],
+ 'targetfilter': [{'values': ['(ou=groups)'],
'equal': True}],
+ 'targetscope': [],
+ 'version 3.0;': [],
+ 'rawaci': complex_aci
+ }
+
+ # You can get a raw ACI
+ raw_aci = aci.getRawAci()
+
+Additional information about ACI
+----------------------------------
+
+-
https://access.redhat.com/documentation/en-US/Red_Hat_Directory_Server/10...
+-
https://access.redhat.com/documentation/en-US/Red_Hat_Directory_Server/10...
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.aci.Aci
+ :members:
+
+.. autoclass:: lib389._entry.EntryAci
+ :members:
diff --git a/src/lib389/doc/source/agreement.rst b/src/lib389/doc/source/agreement.rst
new file mode 100644
index 0000000..8d449dc
--- /dev/null
+++ b/src/lib389/doc/source/agreement.rst
@@ -0,0 +1,27 @@
+Agreement
+==========
+
+Usage example
+--------------
+::
+
+ master = topology.ms["master1"]
+ consumer = topology.ms["consumer1"]
+ # Create
+ repl_agreement = master.agreement.create(suffix=DEFAULT_SUFFIX,
+ host=consumer.host,
+ port=consumer.port)
+ # List
+ ents = master.agreement.list(suffix=DEFAULT_SUFFIX,
+ consumer_host=consumer.host,
+ consumer_port=consumer.port)
+ # Delete
+ ents = master1.agreement.delete(suffix=DEFAULT_SUFFIX)
+
+
+Module documentation
+----------------------
+
+.. autoclass:: lib389.agreement.Agreement
+ :members:
+
diff --git a/src/lib389/doc/source/backend.rst b/src/lib389/doc/source/backend.rst
new file mode 100644
index 0000000..5b4e1f4
--- /dev/null
+++ b/src/lib389/doc/source/backend.rst
@@ -0,0 +1,49 @@
+Backend
+==========
+
+Usage example
+--------------
+::
+
+ from lib389.backend import Backends
+
+ backends = Backends(standalone)
+ backend = backends.create(properties={BACKEND_SUFFIX: 'o=new_suffix', #
mandatory
+ BACKEND_NAME: new_backend, # mandatory
+ BACKEND_SAMPLE_ENTRIES: '001003006'})
+
+ # Create sample entries
+ backend.create_sample_entries(version='001003006')
+
+ backend.delete()
+
+Backend properties
+-------------------
+
+- BACKEND_NAME - 'somename'
+- BACKEND_READONLY - 'on' | 'off'
+- BACKEND_REQ_INDEX - 'on' | 'off'
+- BACKEND_CACHE_ENTRIES - 1 to (2^32 - 1) on 32-bit systems or (2^63 - 1)
+ on 64-bit systems or -1, which means limitless
+- BACKEND_CACHE_SIZE - 500 kilobytes to (2^32 - 1)
+ on 32-bit systems and to (2^63 - 1) on 64-bit systems
+- BACKEND_DNCACHE_SIZE - 500 kilobytes to (2^32 - 1)
+ on 32-bit systems and to (2^63 - 1) on 64-bit systems
+- BACKEND_DIRECTORY - Any valid path to the database instance
+- BACKEND_CHAIN_BIND_DN - DN of the multiplexor
+- BACKEND_CHAIN_BIND_PW - password of the multiplexor
+- BACKEND_CHAIN_URLS - Any valid remote server LDAP URL
+- BACKEND_SUFFIX - 'o=somesuffix'
+- BACKEND_SAMPLE_ENTRIES - version of confir i.e. '001003006'
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.backend.Backends
+ :members:
+ :inherited-members:
+
+.. autoclass:: lib389.backend.Backend
+ :members:
+ :inherited-members:
diff --git a/src/lib389/doc/source/changelog.rst b/src/lib389/doc/source/changelog.rst
new file mode 100644
index 0000000..b51dd20
--- /dev/null
+++ b/src/lib389/doc/source/changelog.rst
@@ -0,0 +1,22 @@
+Changelog
+==========
+
+Usage example
+--------------
+::
+
+ standalone = topology.standalone
+ # Create
+ changelog_dn = standalone.changelog.create()
+ # List
+ changelog_entries = standalone.changelog.list(changelogdn=changelog_dn)
+ # Delete
+ standalone.changelog.delete()
+
+
+Module documentation
+----------------------
+
+.. autoclass:: lib389.changelog.Changelog
+ :members:
+
diff --git a/src/lib389/doc/source/conf.py b/src/lib389/doc/source/conf.py
new file mode 100644
index 0000000..c645db2
--- /dev/null
+++ b/src/lib389/doc/source/conf.py
@@ -0,0 +1,335 @@
+# -*- coding: utf-8 -*-
+#
+# lib389 documentation build configuration file, created by
+# sphinx-quickstart on Wed Aug 23 18:43:30 2017.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath('../../'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.ifconfig',
+ 'sphinx.ext.linkcode'
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'lib389'
+copyright = u'2017, Red Hat, Inc.'
+author = u'Red Hat, Inc.'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = u'1.0.4'
+# The full version, including alpha/beta/rc tags.
+release = u'1.0.4'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = []
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built
documents.
+#keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'nature'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.
+# "<project> v<release> documentation" by default.
+#html_title = u'lib389 v1.0.4'
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (relative to this directory) to use as a favicon of
+# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin
"default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not None, a 'Last updated on:' timestamp is inserted at every page
+# bottom, using the given strftime format.
+# The empty string is equivalent to '%b %d, %Y'.
+#html_last_updated_fmt = None
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is
True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+# 'da', 'de', 'en', 'es', 'fi', 'fr',
'hu', 'it', 'ja'
+# 'nl', 'no', 'pt', 'ro', 'ru', 'sv',
'tr', 'zh'
+#html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# 'ja' uses this config value.
+# 'zh' user can custom change `jieba` dictionary path.
+#html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'lib389doc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+
+# Latex figure (float) alignment
+#'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'lib389.tex', u'lib389 Documentation',
+ u'Red Hat, Inc.', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'lib389', u'lib389 Documentation',
+ [author], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'lib389', u'lib389 Documentation',
+ author, 'lib389', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'https://docs.python.org/': None}
+
+import lib389
+import inspect
+from os.path import relpath, dirname
+
+def linkcode_resolve(domain, info):
+ if domain != 'py':
+ return None
+ if not info['module']:
+ return None
+
+ module_name = info['module']
+ fullname = info['fullname']
+ file_name = module_name.replace('.', '/')
+
+ # Get the module object
+ submodule = sys.modules.get(module_name)
+ if not submodule:
+ return None
+
+ # Get submodules
+ next_submodule = submodule
+ for name in fullname.split('.'):
+ try:
+ next_submodule = getattr(next_submodule, name)
+ except:
+ return None
+
+ # Get line number for the submodule
+ try:
+ _, line = inspect.getsourcelines(next_submodule)
+ except:
+ line = None
+
+ if line:
+ line_fmt = "#_{}".format(line)
+ else:
+ line_fmt = ""
+
+ return "https://pagure.io/lib389/blob/master/f/{}.py{}".format(file_name,
line_fmt)
diff --git a/src/lib389/doc/source/config.rst b/src/lib389/doc/source/config.rst
new file mode 100644
index 0000000..8207d42
--- /dev/null
+++ b/src/lib389/doc/source/config.rst
@@ -0,0 +1,47 @@
+Config
+==========
+
+Usage example
+---------------
+::
+
+ # Set config attribute:
+ standalone.config.set('passwordStorageScheme', 'SSHA')
+
+ # Reset config attribute (by deleting it):
+ standalone.config.reset('passwordStorageScheme')
+
+ # Enable/disable logs (error, access, audit):
+ standalone.config.enable_log('error')
+ standalone.config.disable_log('access')
+
+ # Set loglevel for errors log.
+ # If 'update' set to True, it will add the 'vals' to existing values
in the loglevel attribute
+ standalone.config.loglevel(vals=(LOG_DEFAULT,), service='error',
update=False)
+ # You can get log levels from lib389._constants
+ (LOG_TRACE,
+ LOG_TRACE_PACKETS,
+ LOG_TRACE_HEAVY,
+ LOG_CONNECT,
+ LOG_PACKET,
+ LOG_SEARCH_FILTER,
+ LOG_CONFIG_PARSER,
+ LOG_ACL,
+ LOG_ENTRY_PARSER,
+ LOG_HOUSEKEEPING,
+ LOG_REPLICA,
+ LOG_DEFAULT,
+ LOG_CACHE,
+ LOG_PLUGIN,
+ LOG_MICROSECONDS,
+ LOG_ACL_SUMMARY) = [1 << x for x in (list(range(8)) + list(range(11, 19)))]
+
+ # Set 'nsslapd-accesslog-logbuffering' to 'on' if True, otherwise set
it to 'off'
+ standalone.config.logbuffering(True)
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.config.Config
+ :members:
diff --git a/src/lib389/doc/source/databases.rst b/src/lib389/doc/source/databases.rst
new file mode 100644
index 0000000..3cd1bb1
--- /dev/null
+++ b/src/lib389/doc/source/databases.rst
@@ -0,0 +1,7 @@
+Configuring Databases
+---------------------
+
+.. toctree::
+
+ backend.rst
+ mappingtree.rst
diff --git a/src/lib389/doc/source/dirsrv_log.rst b/src/lib389/doc/source/dirsrv_log.rst
new file mode 100644
index 0000000..1d67854
--- /dev/null
+++ b/src/lib389/doc/source/dirsrv_log.rst
@@ -0,0 +1,30 @@
+DirSrv log
+==========
+
+Usage example
+--------------
+::
+
+ # Get array of all lines (including rotated and compresed logs):
+ standalone.ds_access_log.readlines_archive()
+ standalone.ds_error_log.readlines_archive()
+ # Get array of all lines (without rotated and compresed logs):
+ standalone.ds_access_log.readlines()
+ standalone.ds_error_log.readlines()
+ # Get array of lines that match the regex pattern:
+ standalone.ds_access_log.match_archive('.*fd=.*')
+ standalone.ds_error_log.match_archive('.*fd=.*')
+ standalone.ds_access_log.match('.*fd=.*')
+ standalone.ds_error_log.match('.*fd=.*')
+ # Break up the log line into the specific fields:
+ assert(standalone.ds_error_log.parse_line('[27/Apr/2016:13:46:35.775670167 +1000]
slapd started. Listening on All Interfaces port 54321 for LDAP requests') ==
{'timestamp': '[27/Apr/2016:13:46:35.775670167 +1000]', 'message':
'slapd starte d. Listening on All Interfaces port 54321 for LDAP requests',
'datetime': datetime.datetime(2016, 4, 27, 13, 0, 0, 775670, tzinfo=tzoffset(No
ne, 36000))})
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.dirsrv_log.DirsrvAccessLog
+ :members:
+
+.. autoclass:: lib389.dirsrv_log.DirsrvErrorLog
+ :members:
diff --git a/src/lib389/doc/source/domain.rst b/src/lib389/doc/source/domain.rst
new file mode 100644
index 0000000..31040de
--- /dev/null
+++ b/src/lib389/doc/source/domain.rst
@@ -0,0 +1,21 @@
+Domain
+==========
+
+Usage example
+--------------
+::
+
+ # After the creating a backend, sometimes you don't need a lot of entries under
the created suffix
+ # So instead of using BACKEND_SAMPLE_ENTRIES you can create simple domain entry using
the next object:
+ from lib389.idm.domain import Domain
+ domain = Domain(standalone], 'dc=test,dc=com')
+ domain.create(properties={'dc': 'test', 'description':
'dc=test,dc=com'})
+
+ # It will be deleted with the 'backend.delete()'
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.idm.domain.Domain
+ :members:
diff --git a/src/lib389/doc/source/dseldif.rst b/src/lib389/doc/source/dseldif.rst
new file mode 100644
index 0000000..0718232
--- /dev/null
+++ b/src/lib389/doc/source/dseldif.rst
@@ -0,0 +1,30 @@
+DSE ldif
+==========
+
+Usage example
+--------------
+::
+
+ from lib389.dseldif import DSEldif
+
+ dse_ldif = DSEldif(topo.standalone)
+
+ # Get a list of attribute values under a given entry
+ config_cn = dse_ldif.get(DN_CONFIG, 'cn')
+
+ # Add an attribute under a given entry
+ dse_ldif.add(DN_CONFIG, 'someattr', 'someattr_value')
+
+ # Replace attribute values with a new one under a given entry. It will remove all
previous 'someattr' values
+ dse_ldif.replace(DN_CONFIG, 'someattr', 'someattr_value')
+
+ # Delete attributes under a given entry
+ dse_ldif.delete(DN_CONFIG, 'someattr')
+ dse_ldif.delete(DN_CONFIG, 'someattr', 'someattr_value')
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.dseldif.DSEldif
+ :members:
diff --git a/src/lib389/doc/source/group.rst b/src/lib389/doc/source/group.rst
new file mode 100644
index 0000000..a916748
--- /dev/null
+++ b/src/lib389/doc/source/group.rst
@@ -0,0 +1,43 @@
+Group
+========
+
+Usage example
+--------------
+::
+
+ # group and groups additionaly have 'is_member', 'add_member' and
'remove_member' methods
+ # posixgroup and posixgroups have 'check_member' and 'add_member'
+ from lib389.idm.group import Groups
+ from lib389.idm.posixgroup import PosixGroups
+
+ groups = Groups(standalone, DEFAULT_SUFFIX)
+ posix_groups = PosixGroups(standalone, DEFAULT_SUFFIX)
+ group_properties = {
+ 'cn' : 'group1',
+ 'description' : 'testgroup'
+ }
+ group = groups.create(properties=group_properties)
+
+ # So now you can:
+ # Check the membership - shouldn't we make it consistent?
+ assert(not group.is_member(testuser.dn))
+ assert(not posix_groups.check_member(testuser.dn))
+
+ group.add_member(testuser.dn)
+ posix_groups.add_member(testuser.dn)
+
+ # Remove member - add the method to PosixGroups too?
+ group.remove_member(testuser.dn)
+
+ group.delete():
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.idm.group.Groups
+ :members:
+
+.. autoclass:: lib389.idm.group.Group
+ :members:
+
diff --git a/src/lib389/doc/source/guidelines.rst b/src/lib389/doc/source/guidelines.rst
new file mode 100644
index 0000000..6d80c11
--- /dev/null
+++ b/src/lib389/doc/source/guidelines.rst
@@ -0,0 +1,599 @@
+============================================================
+Guidelines for using pytest and lib389
+============================================================
+The guide covers basic workflow with git, py.test, lib389, python-ldap
+and 1minutetip CLIs.
+
+For a saving place purposes, I'll replace topology_m2.ms["master1"]
+with master1 , etc.
+
+
+Basic workflow
+==============
+
+1. Clone ds repo:
+ + git clone ssh://git@pagure.io/389-ds-base.git
+ + One can change the old repos like this in .git/config files
+ + For ds (dirsrv) use link -
https://pagure.io/389-ds-base.git
+ + For lib389 use link -
https://pagure.io/lib389.git
+
+2. Go to the cloned directory
+3. Create a new branch for your work:
+ ::
+
+ git checkout -b new_test_suite
+4. Check out PEP8 cheat sheet:
+ +
https://gist.github.com/RichardBronosky/454964087739a449da04
+ +
https://www.python.org/dev/peps/pep-0008/
+ + It is not fully mandatory in our project, but let's make our code a
+ bit cleaner for other's sake
+
+5. Use ./dirsrvtests/create_test.py tool to generate new test.py file.
+ + Usage:
+
+ ::
+
+ create_ticket.py -t|--ticket <ticket number> -s|--suite <suite name>
+ [ i|--instances <number of standalone instances> [ -m|--masters
+ <number of masters> -h|--hubs <number of hubs> -c|--consumers
<number
+ of consumers> ] -o|--outputfile]
+
+ Create a test suite script using "-s|--suite" instead of using
+ "-t|–ticket". One day, all 'tickets' will be transferred to
'suites',
+ so try to avoid the 'tickets' and try to find the place in
'suites'
+ for you case. Ask around is you have doubts.
+ Option "-i" can add multiple standalone instances. However, you can
+ not mix "-i" with the replication options(-m, -h ,-c).
+
+
+ + For example:
+
+ ::
+
+ create_test.py -s basic -m 2 -o ./dirsrvtests/tests/suites/basic/basic_test.py
+ # It will create basic_test.py with two masters set up and put the file to right
dir
+
+
+ + If you are creating a test suite, the script will add one test case
+ for you with generated ID in the docstring (and it will check it for
+ uniqueness)
+ + Please, add more ID (to new test cases) with the next command and
+ check if it is unique for other tests
+
+ ::
+
+ python -c 'import uuid; print(uuid.uuid4())'
+
+
+6. Add some fixture(s), if needed. The purpose of test fixtures is to
+ provide a fixed baseline upon which tests can reliably and repeatedly
+ execute.
+
+ + For example:
+
+ ::
+
+ @pytest.fixture
+ def rdn_write_setup(topology_m2):
+ topology_m2.ms["master1"].add_s(ENTRY)
+ def fin():
+ topology_m2.ms["master1"].delete_s(ENTRY_DN)
+ request.addfinalizer(fin)
+
+
+
+ + It will add some entry to the master1 in the beginning of the test
+ case and delete this entry after test case is finished.
+
+7. Add test case(s). It should be defined as function which name
+ starts with "test\_"
+
+ + For example:
+
+ ::
+
+ def test_search_limits_fail(topology, rdn_write_setup):
+
+ + You can put any amount of created fixtures as the arguments
+
+8. Write some good code with encapsulations, assertions etc.
+9. Commit and push your code to your repo:
+
+ ::
+
+ git add ./dirsrvtests/tests/suites/basic/basic_test.py
+ git commit
+ git push $(whoami)
+
+
+10. Test your script:
+
+ ::
+
+ py.test -v -s /mnt/testarea/test/ds/dirsrvtests/suites/basic
+
+
+11. If everything is alright, then create a patch file for a review:
+
+ + Go back to ds or lib389 dir (depends on where you want to send the patch) and do:
+
+ ::
+
+ git checkout master
+ git pull
+ git checkout new_test_suite
+ git rebase master
+ git format-patch -1
+
+
+ + Basic guidelines for the commit message format
+
+ + Separate subject from body with a blank line
+ + Limit the subject line to 50 characters
+ + Capitalizethesubject line
+ + Do not end the subject line with a period
+ + Use the imperative mood in the subject line
+ + Wrap the body at 72 characters
+ + Use the body to explain *what* and *why* vs. *how*
+ + In the end, put a link to the ticket
+ + Add "Reviewed by: ?" line. Example:
+
+ Issue 48085 - Expand the repl acceptance test suite
+ Description: Add 6 more test cases to the replication
+ test suite as a part of the TET to
+ pytest porting initiative.
+ Increase the number of seconds we wait before the results check.
+
+
https://pagure.io/389-ds-base/issue/48085
+
+ Reviewed by: ?
+
+
+12. Fixing Review Issues
+
+ + If there are issues with your patch, git allows you to fix your
+ commits.
+ + If you're not already in that branch
+ + git checkout new_test_suite
+ + Make changes to some file
+ + Add changes to your commit and fix the commit message if necessary
+
+ ::
+
+ git commit -a --amend
+
+ + You can also use “ git rebase -i ” to “squash” or combine several
+ commits into one commit.
+
+
+Fixtures
+=========
+
+Basic info about fixtures -
http://pytest.org/latest/fixture.html#fixtures
+
+Scope
+~~~~~
+
++ the scope for which this fixture is shared, one of “function”
+ (default), “class”, “module”, “session”
++ Use “function”, if you want fixture to be applied for every test
+ case where it appears
++ Use “module”, if you want fixture to be applied for a whole test
+ suite (file you run)
+
+Parametrizing
+~~~~~~~~~~~~~
+
++ Fixture functions can be parametrized in which case they will be
+ called multiple times, each time executing the set of dependent tests,
+ i. e. the tests that depend on this fixture.
++ You should put your params in list and then access it within you
+ fixture with request.param. For example:
+
+ ::
+
+ # First it will test with adding and deleting ENTRY to the first master then to the
second
+ @pytest.fixture(params=[0, 1])
+ def rdn_write_setup(topology_m2):
+ m_num = request.param
+ topology_m2.ms["master{}".format(m_num)].add_s(ENTRY)
+ def fin():
+ topology_m2.ms["master{}".format(m_num)].delete_s(ENTRY_DN)
+ request.addfinalizer(fin)
+
+
+Test cases
+==========
+
+Parametrizing
+~~~~~~~~~~~~~
+
++ The built-in pytest.mark.parametrize decorator enables
+ parameterization of arguments for a test function. For example:
+
+ ::
+
+ ROOTDSE_DEF_ATTR_LIST = ('namingContexts',
+ 'supportedLDAPVersion',
+ 'supportedControl',
+ 'supportedExtension',
+ 'supportedSASLMechanisms',
+ 'vendorName',
+ 'vendorVersion')
+ @pytest.mark.parametrize("rootdse_attr_name", ROOTDSE_DEF_ATTR_LIST)
+ def test_def_rootdse_attr(topology_st, import_example_ldif, rootdse_attr_name):
+ """Tests that operational attributes
+ are not returned by default in rootDSE searches
+ """
+
+ log.info("Assert rootdse search hasn't {}
attr".format(rootdse_attr_name))
+ entries = topology_st.standalone.search_s("", ldap.SCOPE_BASE)
+ entry = str(entries[0])
+ assert rootdse_attr_name not in entry
+
+
++ As you can see, unlike the fixture parametrizing, in the test case
+ you should first put the name of attributes, then the list (or tuple)
+ with values, and then put the attribute to the function declaration.
++ You can specify a few attributes for parametrizing
+
+ ::
+
+ @pytest.mark.parametrize("test_input,expected", [
+ ("3+5", 8),
+ ("2+4", 6),
+ ("6*9", 42),])
+ def test_eval(test_input, expected):
+ assert eval(test_input) == expected
+
+
+Marking test functions and selecting them for a run
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
++ You can “mark” a test function with custom meta data like this:
+
+ ::
+
+ @pytest.mark.ssl
+ def test_search_sec_port():
+ pass # perform some search through sec port
+
+
++ You can also set a module level marker in which case it will be
+ applied to all functions and methods defined in the module:
+
+ ::
+
+ import pytest
+ pytestmark = pytest.mark.ssl
+
+
++ You can then restrict a test run to only run tests marked with ssl:
+
+ ::
+
+ py.test -v -m ssl
+
++ Or the inverse, running all tests except the ssl ones:
+
+ ::
+
+ py.test -v -m "not ssl"
+
++ Select tests based on their node ID
+
+ + You can provide one or more node IDs as positional arguments to
+ select only specified tests. This makes it easy to select tests based
+ on their module, class, method, or function name:
+ + py.test -v test_server.py::test_function1
+ test_server.py::test_function2
+
++ Use -k expr to select tests based on their name
+
+ + You can use the -k command line option to specify an expression
+ which implements a substring match on the test names instead of the
+ exact match on markers that -m provides. This makes it easy to select
+ tests based on their names
+
+ ::
+
+ py.test -v -k search
+ py.test -v -k "search or modify"
+ py.test -v -k "not modify"
+
+Asserting
+~~~~~~~~~
+
++ pytest allows you to use the standard python assert for verifying
+ expectations and values in Python tests. For example, you can write
+ the following:
+
+ ::
+
+ def f():
+ return 3
+ def test_function():
+ assert f() == 4
+
+
++ You can put the message to assert , it will be shown when error
+ appears:
+
+ ::
+
+ assert a % 2 == 0, "value was odd, should be even"
+
+
++ In order to write assertions about raised exceptions, you can use
+ pytest.raises as a context manager like this:
+
+ ::
+
+ import pytest
+ def test_zero_division():
+ with pytest.raises(ZeroDivisionError):
+ 1 / 0
+
+
++ Or even like this, if you expect some particular exception:
+
+ ::
+
+ def test_recursion_depth():
+ with pytest.raises(RuntimeError) as excinfo:
+ def f():
+ f()
+ f()
+ assert 'maximum recursion' in str(excinfo.value)
+
+
+
+Constants
+==========
+
+Basic constants
+~~~~~~~~~~~~~~~
+
+::
+
+ DEFAULT_SUFFIX = “dc=example,dc=com”
+ DN_DM = "cn=Directory Manager"
+ PW_DM = "password"
+ DN_CONFIG = "cn=config"
+ DN_SCHEMA = "cn=schema"
+ DN_LDBM = "cn=ldbm database,cn=plugins,cn=config"
+ DN_CONFIG_LDBM = "cn=config,cn=ldbm database,cn=plugins,cn=config"
+ DN_USERROOT_LDBM = "cn=userRoot,cn=ldbm database,cn=plugins,cn=config"
+ DN_MONITOR = "cn=monitor"
+ DN_MONITOR_SNMP = "cn=snmp,cn=monitor"
+ DN_MONITOR_LDBM = "cn=monitor,cn=ldbm database,cn=plugins,cn=config"
+ CMD_PATH_SETUP_DS = "setup-ds.pl"
+ CMD_PATH_REMOVE_DS = "remove-ds.pl"
+ CMD_PATH_SETUP_DS_ADMIN = "setup-ds-admin.pl"
+ CMD_PATH_REMOVE_DS_ADMIN = "remove-ds-admin.pl"
+
+
+For more info check the source code at
+https://pagure.io/lib389/blob/master/f/lib389/_constants.py . If
+you need a constant, use this kind of import.
+If you need a lot of constants, import with *
+
+::
+
+ from lib389._constants import CONSTANT_YOU_NEED
+ from lib389._constants import *
+
+
+Add, Modify, and Delete Operations
+===================================
+
+Please, use these methods for the operations that can't be performed
+by DSLdapObjects.
+
+::
+
+ # Add an entry
+ USER_DN = 'cn=mreynolds,{}'.format(DEFAULT_SUFFIX)
+ standalone.add_s(Entry((USER_DN, {
+ 'objectclass': 'top person'.split(),
+ 'cn': 'mreynolds',
+ 'sn': 'reynolds',
+ 'userpassword': 'password'
+ })))
+
+ # Modify an entry
+ standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE, 'cn', 'Mark
Reynolds')])
+
+ # Delete an entry
+ standalone.delete_s(USER_DN)
+
+
+Search and Bind Operations
+===================================
+
++ By default when an instance is created and opened, it is already
+ authenticated as the Root DN(Directory Manager).
++ So you can just start searching without having to “bind”
+
+::
+
+ # Search
+ entries = standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, '(cn=*)',
['cn'])
+ for entry in entries:
+ if 'Mark Reynolds' in entry.data['cn']:
+ log.info('Search found "Mark"')
+ print(entry.data['cn'])
+
+ # Anonymous bind
+ bind_dn = ""
+ bind_pwd = ""
+
+ # Bind as our test entry
+ bind_dn = USER_DN
+ bind_pwd = "password"
+
+ # Bind as Directory Manager
+ bind_dn = DN_DM
+ bind_pwd = 1
+
+ standalone.simple_bind_s(bind_dn, bind_pwd)
+
+
+Basic instance operations
+===================================
+
+::
+
+ # First, create a new “instance” of a “DirSrv” object
+ standalone = DirSrv(verbose=False)
+
+ # Set up the instance arguments (note - args_instance is a global dictionary
+ # in lib389, it contains other default values)
+ args_instance[SER_HOST] = HOST_STANDALONE
+ args_instance[SER_PORT] = PORT_STANDALONE
+ args_instance[SER_SERVERID_PROP] = SERVERID_STANDALONE
+ args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX
+ args_standalone = args_instance.copy()
+ # Allocate the instance - initialize the “DirSrv” object with our arguments
+ standalone.allocate(args_standalone)
+ # Check if the instance with the args exists
+ assert not standalone.exists()
+ # Create the instance - this runs setup-ds.pl and starts the server
+ standalone.create()
+
+ # Open the instance - create a connection to the instance,
+ # and authenticates as the Root DN (cn=directory manager)
+ standalone.open()
+ # Done, you can start using the new instance
+ # While working with DirSrv object, you can set 'verbose' parameter to True
in any moment
+ standalone.verbose = True
+ # To remove an instance, simply use:
+ standalone.delete()
+ # Start, Stop, and Restart the Server
+ standalone.start(timeout=10)
+ standalone.stop(timeout=10)
+ standalone.restart(timeout=10)
+
+ # Returns True, if the instance was shutdowned disorderly
+ standalone.detectDisorderlyShutdown()
+
+
+Setting up SSL/TLS
+===================================
+
+::
+
+ from lib389._constants import DEFAULT_SUFFIX, SECUREPORT_STANDALONE1
+
+ standalone.stop()
+
+ # Re-init (create) the nss db
+ # pin.txt is created here and the password randomly generated
+ assert(standalone.nss_ssl.reinit() is True)
+
+ # Create a self signed CA
+ # noise.txt is created here
+ assert(standalone.nss_ssl.create_rsa_ca() is True)
+
+ # Create a key and a cert that is signed by the self signed ca
+ # This will use the hostname from the DS instance, and takes a list of extra names to
take.
+ assert(standalone.nss_ssl.create_rsa_key_and_cert() is True)
+
+ standalone.start()
+
+ # Create "cn=RSA,cn=encryption,cn=config" with next properties:
+ # {'cn': 'RSA', 'nsSSLPersonalitySSL': 'Server-Cert',
'nsSSLActivation': 'on', 'nsSSLToken': 'internal
(software)'}
+ standalone.rsa.create()
+ # Set the secure port and nsslapd-security
+ standalone.config.set('nsslapd-secureport', str(SECUREPORT_STANDALONE1))
+ standalone.config.set('nsslapd-security', 'on')
+ standalone.sslport = SECUREPORT_STANDALONE1
+
+ # Restart to allow certmaps to be re-read: Note, we CAN NOT use post_open
+ standalone.restart(post_open=False)
+
+
+Certification-based authentication
+===================================
+
+You need to setup and turn on SSL first (use the previous chapter).
+
+::
+
+ from lib389.config import CertmapLegacy
+
+ standalone.stop()
+
+ # Create a user
+ assert(standalone.nss_ssl.create_rsa_user('testuser') is True)
+
+ # Get the details of where the key and crt are
+ # {'ca': ca_path, 'key': key_path, 'crt': crt_path}
+ tls_locs = standalone.nss_ssl.get_rsa_user('testuser')
+
+ standalone.start()
+
+ # Create user in the directory
+ users = UserAccounts(standalone, DEFAULT_SUFFIX)
+ users.create(properties={
+ 'uid': 'testuser',
+ 'cn' : 'testuser',
+ 'sn' : 'user',
+ 'uidNumber' : '1000',
+ 'gidNumber' : '2000',
+ 'homeDirectory' : '/home/testuser'
+ })
+
+ # Turn on the certmap
+ cm = CertmapLegacy(standalone)
+ certmaps = cm.list()
+ certmaps['default']['DNComps'] = ''
+ certmaps['default']['FilterComps'] = ['cn']
+ certmaps['default']['VerifyCert'] = 'off'
+ cm.set(certmaps)
+
+ # Restart to allow certmaps to be re-read: Note, we CAN NOT use post_open
+ standalone.restart(post_open=False)
+
+ # Now attempt a bind with TLS external
+ conn = standalone.openConnection(saslmethod='EXTERNAL', connOnly=True,
certdir=standalone.get_cert_dir(), userkey=tls_locs['key'],
usercert=tls_locs['crt'])
+
+ assert(conn.whoami_s() == "dn: uid=testuser,ou=People,dc=example,dc=com")
+
+
+Replication
+===================================
+
+Basic configuration
+
++ After the instance is created, you can enable it for replication and
+ set up a replication agreement.
+
+::
+
+ from lib389.replica import Replicas
+
+ # Enable replication
+ replicas = Replicas(standalone)
+ replica = replicas.enable(suffix=DEFAULT_SUFFIX,
+ role=REPLICAROLE_MASTER,
+ replicaID=REPLICAID_MASTER_1)
+ # Set up replication agreement properties
+ properties = {RA_NAME: r'meTo_{}:{}'.format(master2.host,
port=master2.port),
+ RA_BINDDN: defaultProperties[REPLICATION_BIND_DN],
+ RA_BINDPW: defaultProperties[REPLICATION_BIND_PW],
+ RA_METHOD: defaultProperties[REPLICATION_BIND_METHOD],
+ RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
+
+ # Create the agreement
+ repl_agreement = standalone.agreement.create(suffix=DEFAULT_SUFFIX,
+ host=master2.host,
+ port=master2.port,
+ properties=properties)
+ # “master2” refers to another, already created, DirSrv instance(like “standalone”)
+ # “repl_agreement” is the “DN” of the newly created agreement - this DN is needed
later to do certain tasks
+
+ # Initialize the agreement, wait for it complete, and test that replication is really
working
+ standalone.agreement.init(DEFAULT_SUFFIX, master2.host, master2.port)
+ standalone.waitForReplInit(repl_agreement)
+ assert standalone.testReplication(DEFAULT_SUFFIX, master2)
diff --git a/src/lib389/doc/source/identitymanagement.rst
b/src/lib389/doc/source/identitymanagement.rst
new file mode 100644
index 0000000..391a691
--- /dev/null
+++ b/src/lib389/doc/source/identitymanagement.rst
@@ -0,0 +1,10 @@
+Identity Management
+-------------------
+
+.. toctree::
+
+ user.rst
+ group.rst
+ domain.rst
+ organisationalunit.rst
+ services.rst
diff --git a/src/lib389/doc/source/index.rst b/src/lib389/doc/source/index.rst
new file mode 100644
index 0000000..e5445f8
--- /dev/null
+++ b/src/lib389/doc/source/index.rst
@@ -0,0 +1,51 @@
+.. lib389 documentation master file, created by
+ sphinx-quickstart on Wed Aug 23 18:43:30 2017.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Welcome to lib389's documentation!
+==================================
+
+Lib389 is a python base library for managing Directory servers.
+It was initially created to help writing tests of DS. It can also used
+to create new administration CLIs. This document is focusing on writing
+tests for the Directory Server. Lib389 can be used to generate individual
+testcases(tickets), and suites that test a range of functionality/feature.
+
+
+Contents
+========
+
+.. toctree::
+ :maxdepth: 2
+
+ guidelines.rst
+ Replication <replication.rst>
+ Configuring Databases <databases.rst>
+ Access Control <accesscontrol.rst>
+
+Work in progress
+-----------------
+.. toctree::
+ :maxdepth: 2
+
+ Identity Management <identitymanagement.rst>
+ need_to_be_triaged.rst
+
+
+Contact us
+==========
+
+If you have any issue or a question, you have a few options:
+
++ write an email to 389-devel(a)lists.fedoraproject.org
++ ask on
irc.freenode.net channel #389
++ check (or create a new) issues on
https://pagure.io/lib389
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/src/lib389/doc/source/indexes.rst b/src/lib389/doc/source/indexes.rst
new file mode 100644
index 0000000..3980028
--- /dev/null
+++ b/src/lib389/doc/source/indexes.rst
@@ -0,0 +1,42 @@
+Indexes
+==========
+
+Usage example
+--------------
+::
+
+ from lib389.index import Indexes
+
+ indexes = Indexes(standalone)
+
+ # create and delete a default index.
+ index = indexes.create(properties={
+ 'cn': 'modifytimestamp',
+ 'nsSystemIndex': 'false',
+ 'nsIndexType': 'eq'
+ })
+
+ default_index_list = indexes.list()
+ found = False
+ for i in default_index_list:
+ if i.dn.startswith('cn=modifytimestamp'):
+ found = True
+ assert found
+ index.delete()
+
+ default_index_list = indexes.list()
+ found = False
+ for i in default_index_list:
+ if i.dn.startswith('cn=modifytimestamp'):
+ found = True
+ assert not found
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.index.Index
+ :members:
+
+.. autoclass:: lib389.index.Indexes
+ :members:
diff --git a/src/lib389/doc/source/ldclt.rst b/src/lib389/doc/source/ldclt.rst
new file mode 100644
index 0000000..046a3b5
--- /dev/null
+++ b/src/lib389/doc/source/ldclt.rst
@@ -0,0 +1,42 @@
+Config
+==========
+
+Usage example
+--------------
+::
+
+ This class will allow general usage of ldclt. It's not meant to expose all the
functions. Just use ldclt for that.
+ # Creates users as user<min through max>. Password will be set to
password<number>
+ # This will automatically work with the bind loadtest.
+ # Template
+ # objectClass: top
+ # objectclass: person
+ # objectClass: organizationalPerson
+ # objectClass: inetorgperson
+ # objectClass: posixAccount
+ # objectClass: shadowAccount
+ # sn: user[A]
+ # cn: user[A]
+ # givenName: user[A]
+ # description: description [A]
+ # userPassword: user[A]
+ # mail: user[A](a)example.com
+ # uidNumber: 1[A]
+ # gidNumber: 2[A]
+ # shadowMin: 0
+ # shadowMax: 99999
+ # shadowInactive: 30
+ # shadowWarning: 7
+ # homeDirectory: /home/user[A]
+ # loginShell: /bin/false
+ topology.instance.ldclt.create_users('ou=People,{}'.format(DEFAULT_SUFFIX),
max=1999)
+
+ # Run the load test for a few rounds
+ topology.instance.ldclt.bind_loadtest('ou=People,{}'.format(DEFAULT_SUFFIX),
max=1999)
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.ldclt.Ldclt
+ :members:
diff --git a/src/lib389/doc/source/mappingtree.rst
b/src/lib389/doc/source/mappingtree.rst
new file mode 100644
index 0000000..de6843c
--- /dev/null
+++ b/src/lib389/doc/source/mappingtree.rst
@@ -0,0 +1,31 @@
+Mapping Tree
+=============
+
+Usage example
+---------------
+::
+
+ # In the majority of test cases, it is better to use 'Backends' for the
operation,
+ # because it creates the mapping tree for you. Though if you need to create a mapping
tree, you can.
+ # Just work with it as with usual DSLdapObject and DSLdapObjects
+ # For instance:
+ from lib389.mappingTree import MappingTrees
+ mts = MappingTrees(standalone)
+ mt = mts.create(properties={
+ 'cn': ["dc=newexample,dc=com",],
+ 'nsslapd-state' : 'backend',
+ 'nsslapd-backend' : 'someRoot',
+ })
+ # It will be deleted with the 'backend.delete()'
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.mappingTree.MappingTrees
+ :members:
+ :inherited-members:
+
+.. autoclass:: lib389.mappingTree.MappingTree
+ :members:
+ :inherited-members:
diff --git a/src/lib389/doc/source/monitor.rst b/src/lib389/doc/source/monitor.rst
new file mode 100644
index 0000000..5a02510
--- /dev/null
+++ b/src/lib389/doc/source/monitor.rst
@@ -0,0 +1,19 @@
+Monitor
+==========
+
+Usage example
+--------------
+::
+
+ # Monitor and MonitorLDBM are the simple DSLdapObject things.
+ # You can use all methods from chapter above to get current server performance
detail
+ version = standalone.monitor.get_attr_val('version')
+ dbcachehit = standalone.monitorldbm.get_attr_val('dbcachehit')
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.monitor.Monitor
+ :members:
+
diff --git a/src/lib389/doc/source/need_to_be_triaged.rst
b/src/lib389/doc/source/need_to_be_triaged.rst
new file mode 100644
index 0000000..fbbed03
--- /dev/null
+++ b/src/lib389/doc/source/need_to_be_triaged.rst
@@ -0,0 +1,20 @@
+Need to be triaged
+-------------------
+
+.. toctree::
+
+ backend.rst
+ config.rst
+ dirsrv_log.rst
+ dseldif.rst
+ paths.rst
+ indexes.rst
+ ldclt.rst
+ mappingtree.rst
+ monitor.rst
+ passwd.rst
+ plugin.rst
+ rootdse.rst
+ schema.rst
+ task.rst
+ utils.rst
diff --git a/src/lib389/doc/source/organisationalunit.rst
b/src/lib389/doc/source/organisationalunit.rst
new file mode 100644
index 0000000..e232f08
--- /dev/null
+++ b/src/lib389/doc/source/organisationalunit.rst
@@ -0,0 +1,41 @@
+Organisational Unit
+====================
+
+Usage example
+--------------
+::
+
+ # Don't forget that Services requires created rdn='ou=Services'
+ # This you can create with OrganisationalUnits
+
+ from lib389.idm.organisationalunit import OrganisationalUnits
+ from lib389.idm.services import ServiceAccounts
+
+ ous = OrganisationalUnits(standalone, DEFAULT_SUFFIX)
+ services = ServiceAccounts(standalone, DEFAULT_SUFFIX)
+
+ # Create the OU for them
+ ous.create(properties={
+ 'ou': 'Services',
+ 'description': 'Computer Service accounts which request DS
bind',
+ })
+
+ # Now, we can create the services from here.
+ service = services.create(properties={
+ 'cn': 'testbind',
+ 'userPassword': 'Password1'
+ })
+
+ conn = service.bind('Password1')
+ conn.unbind_s()
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.idm.organisationalunit.OrganisationalUnits
+ :members:
+
+.. autoclass:: lib389.idm.organisationalunit.OrganisationalUnit
+ :members:
+
diff --git a/src/lib389/doc/source/passwd.rst b/src/lib389/doc/source/passwd.rst
new file mode 100644
index 0000000..5b60d60
--- /dev/null
+++ b/src/lib389/doc/source/passwd.rst
@@ -0,0 +1,33 @@
+LDCLT
+==========
+
+Usage example
+--------------
+::
+
+ from lib389.passwd import password_hash, password_generate
+
+ bindir = standalone.ds_paths.bin_dir
+ PWSCHEMES = [
+ 'SHA1',
+ 'SHA256',
+ 'SHA512',
+ 'SSHA',
+ 'SSHA256',
+ 'SSHA512',
+ 'PBKDF2_SHA256',
+ ]
+
+ # Generate password
+ raw_secure_password = password_generate()
+
+ # Encrypt the password
+ # default scheme is 'SSHA512'
+ secure_password = password_hash(raw_secure_password, scheme='SSHA256',
bin_dir=bindir)
+
+
+Module documentation
+-----------------------
+
+.. automodule:: lib389.passwd
+ :members:
diff --git a/src/lib389/doc/source/paths.rst b/src/lib389/doc/source/paths.rst
new file mode 100644
index 0000000..39680c6
--- /dev/null
+++ b/src/lib389/doc/source/paths.rst
@@ -0,0 +1,42 @@
+Paths
+==========
+
+Usage example
+--------------
+::
+
+ # You can get any variable from the list bellow. Like this:
+ product = standalone.ds_paths.product
+
+ variables = [
+ 'product',
+ 'version',
+ 'user',
+ 'group',
+ 'root_dn',
+ 'prefix',
+ 'bin_dir',
+ 'sbin_dir',
+ 'lib_dir',
+ 'data_dir',
+ 'tmp_dir',
+ 'sysconf_dir',
+ 'config_dir',
+ 'schema_dir',
+ 'cert_dir',
+ 'local_state_dir',
+ 'run_dir',
+ 'lock_dir',
+ 'log_dir',
+ 'inst_dir',
+ 'db_dir',
+ 'backup_dir',
+ 'ldif_dir',
+ 'initconfig_dir',
+ ]
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.paths.Paths
+ :members:
diff --git a/src/lib389/doc/source/plugin.rst b/src/lib389/doc/source/plugin.rst
new file mode 100644
index 0000000..8592291
--- /dev/null
+++ b/src/lib389/doc/source/plugin.rst
@@ -0,0 +1,35 @@
+Plugin
+==========
+
+You can take plugin constant names here -
https://pagure.io/lib389/blob/master/f/lib389/_constants.py#_164
+
+Usage example
+--------------
+::
+
+ # Plugin and Plugins additionaly have 'enable', 'disable' and
'status' methods
+ # Here I show you basic way to work with it. Additional methods of complex plugins
will be described in subchapters
+
+ from lib389.plugin import Plugins, ACLPlugin
+ from lib389._constants import PLUGIN_ACL
+
+ # You can just enable/disable plugins from Plugins interface
+ plugins = Plugins(standalone)
+ plugins.enable(PLUGIN_ACL)
+
+ # Or you can first 'get' it and then work with it (make sense if your plugin
is a complex one)
+ aclplugin = ACLPlugin(standalone)
+
+ aclplugin.enable()
+
+ aclplugin.disable()
+
+ # True if nsslapd-pluginEnabled is 'on', False otherwise - change the name?
+ assert(uniqplugin.status())
+
+
+Module documentation
+-----------------------
+
+.. automodule:: lib389.plugins
+ :members:
diff --git a/src/lib389/doc/source/replica.rst b/src/lib389/doc/source/replica.rst
new file mode 100644
index 0000000..75acdb8
--- /dev/null
+++ b/src/lib389/doc/source/replica.rst
@@ -0,0 +1,59 @@
+Replica
+==========
+
+Usage example
+--------------
+::
+
+ from lib389.replica import Replicas
+
+ replicas = Replicas(standalone)
+ # Enable replication
+ # - changelog will be created
+ # - replica manager will be with the defaults
+ # - replica.create() will be executed
+ replica = replicas.enable(suffix=DEFAULT_SUFFIX,
+ role=REPLICAROLE_MASTER,
+ replicaID=REPLICAID_MASTER_1)
+ # Roles - REPLICAROLE_MASTER, REPLICAROLE_HUB, and REPLICAROLE_CONSUMER
+ # For masters and hubs you can use the constants REPLICAID_MASTER_X and
REPLICAID_HUB_X
+ # Change X for a number from 1 to 100 - for role REPLICAROLE_MASTER only
+
+ # Disable replication
+ # - agreements and replica entry will be deleted
+ # - changelog is not deleted (but should?)
+ replicas.disable(suffix=DEFAULT_SUFFIX)
+
+ # Get RUV entry
+ replicas.get_ruv_entry()
+
+ # Get DN
+ replicas.get_dn(suffix)
+
+ # Promote
+ replicas.promote(suffix=DEFAULT_SUFFIX,
+ newrole=REPLICAROLE_MASTER,
+ binddn=REPL_BINDDN,
+ rid=REPLICAID_MASTER_1)
+ # Demote
+ replicas.demote(suffix=DEFAULT_SUFFIX,
+ newrole=REPLICAROLE_CONSUMER)
+ # Test, that replication works
+ replicas.test(master2)
+
+ # Additional replica object methods
+ # Get role
+ replica.get_role()
+
+ replica.deleteAgreements()
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.replica.Replicas
+ :members:
+ :inherited-members:
+
+.. autoclass:: lib389.replica.Replica
+ :members:
+ :inherited-members:
diff --git a/src/lib389/doc/source/replication.rst
b/src/lib389/doc/source/replication.rst
new file mode 100644
index 0000000..0a8ab7b
--- /dev/null
+++ b/src/lib389/doc/source/replication.rst
@@ -0,0 +1,9 @@
+Replication
+-----------
+
+.. toctree::
+
+ agreement.rst
+ changelog.rst
+ replica.rst
+ repltools.rst
diff --git a/src/lib389/doc/source/repltools.rst b/src/lib389/doc/source/repltools.rst
new file mode 100644
index 0000000..ce28197
--- /dev/null
+++ b/src/lib389/doc/source/repltools.rst
@@ -0,0 +1,43 @@
+Replication Tools
+==================
+
+Usage example
+--------------
+::
+
+ from lib389.repltools import ReplTools
+
+ # Gather all the CSN strings from the access and verify all of those CSNs exist on
all the other replicas.
+ # dirsrv_replicas - a list of DirSrv objects. The list must begin with master
replicas
+ # ignoreCSNs - an optional string of csns to be ignored
+ # if the caller knows that some csns can differ eg.:
'57e39e72000000020000|vucsn-57e39e76000000030000'
+ ReplTools.checkCSNs([master1, master2], ignoreCSNs=None)
+
+ # Find and measure the convergence of entries from a replica, and
+ # print a report on how fast all the "ops" replicated to the other
replicas.
+ # suffix - Replicated suffix
+ # ops - A list of "operations" to search for in the access logs
+ # replica - Dirsrv object where the entries originated
+ # all_replicas - A list of Dirsrv replicas
+ # It returns - The longest time in seconds for an operation to fully converge
+ longest_time = ReplTools.replConvReport(DEFAULT_SUFFIX, ops, master1, [master1,
master2])
+
+ # Take a list of DirSrv Objects and check to see if all of the present
+ # replication agreements are idle for a particular backend
+ assert(ReplTools.replIdle([master1, master2], suffix=DEFAULT_SUFFIX))
+ defaultProperties = {
+ REPLICATION_BIND_DN: "cn=replrepl,cn=config",
+ REPLICATION_BIND_PW
+
+ # Create an entry that will be used to bind as replication manager
+ ReplTools.createReplManager(standalone,
+ repl_manager_dn=defaultProperties[REPLICATION_BIND_DN],
+ repl_manager_pw=defaultProperties[REPLICATION_BIND_PW])
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.repltools.ReplTools
+ :members:
+
diff --git a/src/lib389/doc/source/rootdse.rst b/src/lib389/doc/source/rootdse.rst
new file mode 100644
index 0000000..51ec8b8
--- /dev/null
+++ b/src/lib389/doc/source/rootdse.rst
@@ -0,0 +1,25 @@
+Root DSE
+==========
+
+Usage example
+--------------
+::
+
+ # Get attribute values of 'supportedSASLMechanisms'
+ standalone.rootdse.supported_sasl()
+
+ # Returns True or False
+ assert(standalone.rootdse.supports_sasl_gssapi()
+ assert(standalone.rootdse.supports_sasl_ldapssotoken()
+ assert(standalone.rootdse.supports_sasl_plain()
+ assert(standalone.rootdse.supports_sasl_external()
+ assert(standalone.rootdse.supports_exop_whoami()
+ assert(standalone.rootdse.supports_exop_ldapssotoken_request()
+ assert(standalone.rootdse.supports_exop_ldapssotoken_revoke()
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.rootdse.RootDSE
+ :members:
diff --git a/src/lib389/doc/source/schema.rst b/src/lib389/doc/source/schema.rst
new file mode 100644
index 0000000..bcff164
--- /dev/null
+++ b/src/lib389/doc/source/schema.rst
@@ -0,0 +1,62 @@
+Schema
+==========
+
+Usage example
+--------------
+::
+
+ # Get the schema as an LDAP entry
+ schema = standalone.schema.get_entry()
+
+ # Get the schema as a python-ldap SubSchema object
+ subschema = standalone.schema.get_subschema()
+
+ # Get a list of the schema files in the instance schemadir
+ schema_files = standalone.schema.list_files()
+
+
+ # Convert the given schema file name to its python-ldap format suitable for passing
to ldap.schema.SubSchema()
+ parsed = standalone.schema.file_to_ldap('/full/path/to/file.ldif')
+
+ # Convert the given schema file name to its python-ldap format ldap.schema.SubSchema
object
+ parsed = standalone.schema.file_to_subschema('/full/path/to/file.ldif')
+
+ # Add a schema element to the schema
+ standalone.schema.add_schema(attr, val)
+
+ # Delete a schema element from the schema
+ standalone.schema.del_schema(attr, val)
+
+ # Add 'attributeTypes' definition to the schema
+ standalone.schema.add_attribute(attributes)
+
+ # Add 'objectClasses' definition to the schema
+ standalone.schema.add_objectclass(objectclasses)
+
+ # Get a schema nsSchemaCSN attribute
+ schema_csn = standalone.schema.get_schema_csn()
+
+ # Get a list of ldap.schema.models.ObjectClass objects for all objectClasses
supported by this instance
+ objectclasses = standalone.schema.get_objectclasses()
+
+ # Get a list of ldap.schema.models.AttributeType objects for all attributeTypes
supported by this instance
+ attributetypes = standalone.schema.get_attributetypes()
+
+ # Get a list of the server defined matching rules
+ matchingrules = standalone.schema.get_matchingrules()
+
+ # Get a single matching rule instance that matches the mr_name. Returns None if the
matching rule doesn't exist
+ matchingrule = standalone.schema.query_matchingrule(matchingrule_name)
+
+ # Get a single ObjectClass instance that matches objectclassname. Returns None if the
objectClass doesn't exist
+ objectclass = standalone.schema.query_objectclass(objectclass_name)
+
+ # Returns a tuple of the AttributeType, and what objectclasses may or must take this
attributeType. Returns None if attributetype doesn't
+ (attributetype, may, must) =
standalone.schema.query_attributetype(attributetype_name)
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.schema.Schema
+ :members:
diff --git a/src/lib389/doc/source/services.rst b/src/lib389/doc/source/services.rst
new file mode 100644
index 0000000..b556e8a
--- /dev/null
+++ b/src/lib389/doc/source/services.rst
@@ -0,0 +1,40 @@
+Services
+==========
+
+Usage example
+--------------
+::
+
+ # Don't forget that Services requires created rdn='ou=Services'
+ # This you can create with OrganisationalUnits
+
+ from lib389.idm.organisationalunit import OrganisationalUnits
+ from lib389.idm.services import ServiceAccounts
+
+ ous = OrganisationalUnits(standalone, DEFAULT_SUFFIX)
+ services = ServiceAccounts(standalone, DEFAULT_SUFFIX)
+
+ # Create the OU for them
+ ous.create(properties={
+ 'ou': 'Services',
+ 'description': 'Computer Service accounts which request DS
bind',
+ })
+
+ # Now, we can create the services from here.
+ service = services.create(properties={
+ 'cn': 'testbind',
+ 'userPassword': 'Password1'
+ })
+
+ conn = service.bind('Password1')
+ conn.unbind_s()
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.idm.services.ServiceAccounts
+ :members:
+
+.. autoclass:: lib389.idm.services.ServiceAccount
+ :members:
diff --git a/src/lib389/doc/source/task.rst b/src/lib389/doc/source/task.rst
new file mode 100644
index 0000000..9adf96c
--- /dev/null
+++ b/src/lib389/doc/source/task.rst
@@ -0,0 +1,59 @@
+Indexes
+==========
+
+Besides the predefined tasks (which described in a chapter below) you can create
+your own task objects with specifying a DN
(
https://pagure.io/lib389/blob/master/f/lib389/_constants.py#_134)
+
+Usage example
+--------------
+::
+
+ from lib389.tasks import Task
+
+ newtask = Task(instance, dn) # Should we create Tasks and put the precious to
TasksLegacy?
+
+ newtask.create(rdn, properties, basedn)
+
+ # Check if the task is complete
+ assert(newtask.is_complete())
+
+ # Check task's exit code if task is complete, else None
+ if newtask.is_complete():
+ exit_code = newtask.get_exit_code()
+
+ # Wait until task is complete
+ newtask.wait()
+
+ # If True, waits for the completion of the task before to return
+ args = {TASK_WAIT: True}
+
+ # Some tasks ca be found only under old object. You can access them with this:
+ standalone.tasks.importLDIF(DEFAULT_SUFFIX, path_ro_ldif, args)
+ standalone.tasks.exportLDIF(DEFAULT_SUFFIX, benamebase=None,
output_file=path_to_ldif, args)
+ standalone.tasks.db2bak(backup_dir, args)
+ standalone.tasks.bak2db(bename=None, backup_dir, args)
+ standalone.tasks.reindex(suffix=None, benamebase=None, attrname=None, args)
+ standalone.tasks.fixupMemberOf(suffix=None, benamebase=None, filt=None, args)
+ standalone.tasks.fixupTombstones(bename=None, args)
+ standalone.tasks.automemberRebuild(suffix=DEFAULT_SUFFIX, scope='sub',
filterstr='objectclass=top', args)
+ standalone.tasks.automemberExport(suffix=DEFAULT_SUFFIX, scope='sub',
fstr='objectclass=top', ldif_out=None, args)
+ standalone.tasks.automemberMap(ldif_in=None, ldif_out=None, args)
+ standalone.tasks.fixupLinkedAttrs(linkdn=None, args)
+ standalone.tasks.schemaReload(schemadir=None, args)
+ standalone.tasks.fixupWinsyncMembers(suffix=DEFAULT_SUFFIX,
fstr='objectclass=top', args)
+ standalone.tasks.syntaxValidate(suffix=DEFAULT_SUFFIX,
fstr='objectclass=top', args)
+ standalone.tasks.usnTombstoneCleanup(suffix=DEFAULT_SUFFIX, bename=None,
maxusn_to_delete=None, args)
+ standalone.tasks.sysconfigReload(configfile=None, logchanges=None, args)
+ standalone.tasks.cleanAllRUV(suffix=None, replicaid=None, force=None, args)
+ standalone.tasks.abortCleanAllRUV(suffix=None, replicaid=None, certify=None, args)
+ standalone.tasks.upgradeDB(nsArchiveDir=None, nsDatabaseType=None,
nsForceToReindex=None, args)
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.tasks.Tasks
+ :members:
+
+.. autoclass:: lib389.tasks.Task
+ :members:
diff --git a/src/lib389/doc/source/user.rst b/src/lib389/doc/source/user.rst
new file mode 100644
index 0000000..6d59e91
--- /dev/null
+++ b/src/lib389/doc/source/user.rst
@@ -0,0 +1,50 @@
+User Accounts
+=============
+
+Usage example
+--------------
+::
+
+ # There is a basic way to work with it
+ from lib389.idm.user import UserAccounts
+ users = UserAccounts(standalone, DEFAULT_SUFFIX)
+ user_properties = {
+ 'uid': USER_NAME,
+ 'cn' : USER_NAME,
+ 'sn' : USER_NAME,
+ 'userpassword' : USER_PWD,
+ 'uidNumber' : '1000',
+ 'gidNumber' : '2000',1
+ 'homeDirectory' : '/home/{}'.format(USER_NAME)
+ }
+ testuser = users.create(properties=user_properties)
+
+ # After this you can:
+ # Get the list of them
+ users.list()
+
+ # Get some user:
+ testuser = users.get('testuser')
+ # or
+ testuser = users.list()[0] # You can loop through 'for user in users:'
+
+ # Set some attribute to the entry
+ testuser.set('userPassword', 'password')
+
+ # Bind as the user
+ conn = testuser.bind('password') # It will create a new connection
+ conn.modify_s()
+ conn.unbind_s()
+
+ # Delete
+ testuser.delete()
+
+
+Module documentation
+-----------------------
+
+.. autoclass:: lib389.idm.user.UserAccounts
+ :members:
+
+.. autoclass:: lib389.idm.user.UserAccount
+ :members:
diff --git a/src/lib389/doc/source/utils.rst b/src/lib389/doc/source/utils.rst
new file mode 100644
index 0000000..9e7865f
--- /dev/null
+++ b/src/lib389/doc/source/utils.rst
@@ -0,0 +1,23 @@
+Utils
+==========
+
+Usage example
+--------------
+::
+
+ standalone.ldif2db(bename, suffixes, excludeSuffixes, encrypt, import_file)
+ standalone.db2ldif(bename, suffixes, excludeSuffixes, encrypt, repl_data,
outputfile)
+ standalone.bak2db(archive_dir,bename=None)
+ standalone.db2bak(archive_dir)
+ standalone.db2index(bename=None, suffixes=None, attrs=None, vlvTag=None)
+ standalone.dbscan(bename=None, index=None, key=None, width=None, isRaw=False)
+
+ # Generate a simple ldif file using the dbgen.pl script, and set the ownership and
permissions to match the user that the server runs as
+ standalone.buildLDIF(number_of_entries, path_to_ldif,
suffix='dc=example,dc=com')
+
+
+Module documentation
+-----------------------
+
+.. automodule:: lib389.utils
+ :members:
diff --git a/src/lib389/dsadmin.pylintrc b/src/lib389/dsadmin.pylintrc
new file mode 100644
index 0000000..5bef74b
--- /dev/null
+++ b/src/lib389/dsadmin.pylintrc
@@ -0,0 +1,236 @@
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add <file or directory> to the black list. It should be a base name, not a
+# path. You may set this option multiple times.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifier separated by comma (,) or put this option
+# multiple time.
+disable=C0103,C0111,C0321,C0324
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=text
+
+# Include message's id in output
+include-ids=yes
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (R0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) *
10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (R0004).
+comment=no
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=150
+
+# Maximum number of lines in a module
+max-module-lines=3000
+
+# String used as indentation unit. This is usually " " (4 spaces) or
"\t" (1
+# tab).
+indent-string=' '
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching names used for dummy variables (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,apply,input
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_,dn
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject
+
+# When zope mode is activated, add a predefined set of Zope acquired attributes
+# to generated-members.
+zope=no
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed.
+generated-members=REQUEST,acl_users,aq_parent
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branchs=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
diff --git a/src/lib389/lib389/__init__.py b/src/lib389/lib389/__init__.py
new file mode 100644
index 0000000..39e504f
--- /dev/null
+++ b/src/lib389/lib389/__init__.py
@@ -0,0 +1,3189 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""The lib389 module.
+
+
+ IMPORTANT: Ternary operator syntax is unsupported on RHEL5
+ x if cond else y #don't!
+
+ The lib389 functionalities are split in various classes
+ defined in brookers.py
+
+ TODO: reorganize method parameters according to SimpleLDAPObject
+ naming: filterstr, attrlist
+"""
+try:
+ from subprocess import Popen, PIPE, STDOUT
+ HASPOPEN = True
+except ImportError:
+ import popen2
+ HASPOPEN = False
+
+import io
+import sys
+import os
+import stat
+import pwd
+import grp
+import os.path
+import base64
+import socket
+import ldif
+import re
+import ldap
+import ldapurl
+import time
+import operator
+import shutil
+import datetime
+import logging
+import decimal
+import glob
+import tarfile
+import subprocess
+import collections
+import signal
+import errno
+from shutil import copy2
+try:
+ # There are too many issues with this on EL7
+ # Out of the box, it's just outright broken ...
+ import six.moves.urllib.request
+ import six.moves.urllib.parse
+ import six.moves.urllib.error
+ import six
+except ImportError:
+ pass
+from ldap.ldapobject import SimpleLDAPObject
+from ldap.cidict import cidict
+from ldap import LDAPError
+# file in this package
+
+from lib389._constants import *
+from lib389.properties import *
+from lib389._entry import Entry
+from lib389._replication import CSN, RUV
+from lib389._ldifconn import LDIFConn
+from lib389.tools import DirSrvTools
+from lib389.mit_krb5 import MitKrb5
+from lib389.utils import (
+ isLocalHost,
+ is_a_dn,
+ normalizeDN,
+ suffixfilt,
+ escapeDNValue,
+ update_newhost_with_fqdn,
+ formatInfData,
+ ensure_bytes,
+ ensure_str)
+from lib389.paths import Paths
+
+# mixin
+# from lib389.tools import DirSrvTools
+
+from lib389.exceptions import *
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+if MAJOR >= 3 or (MAJOR == 2 and MINOR >= 7):
+ from ldap.controls.simple import GetEffectiveRightsControl
+ from lib389._controls import DereferenceControl
+
+RE_DBMONATTR = re.compile(r'^([a-zA-Z]+)-([1-9][0-9]*)$')
+RE_DBMONATTRSUN = re.compile(r'^([a-zA-Z]+)-([a-zA-Z]+)$')
+
+# This controls pyldap debug levels
+TRACE_LEVEL = 0
+
+# My logger
+log = logging.getLogger(__name__)
+
+# Initiate the paths object here. Should this be part of the DirSrv class
+# for submodules?
+def wrapper(f, name):
+ """
+ Wrapper of all superclass methods using lib389.Entry.
+ @param f - DirSrv method inherited from SimpleLDAPObject
+ @param name - method to call
+
+ This seems to need to be an unbound method, that's why it's outside of
+ DirSrv. Perhaps there is some way to do this with the new classmethod
+ or staticmethod of 2.4.
+
+ We replace every call to a method in SimpleLDAPObject (the superclass
+ of DirSrv) with a call to inner. The f argument to wrapper is the bound
+ method of DirSrv (which is inherited from the superclass). Bound means
+ that it will implicitly be called with the self argument, it is not in
+ the args list. name is the name of the method to call. If name is a
+ method that returns entry objects (e.g. result), we wrap the data returned
+ by an Entry class. If name is a method that takes an entry argument, we
+ extract the raw data from the entry object to pass in.
+ """
+ def inner(*args, **kwargs):
+ if name == 'result':
+ objtype, data = f(*args, **kwargs)
+ # data is either a 2-tuple or a list of 2-tuples
+ # print data
+ if data:
+ if isinstance(data, tuple):
+ return objtype, Entry(data)
+ elif isinstance(data, list):
+ # AD sends back these search references
+ # if objtype == ldap.RES_SEARCH_RESULT and \
+ # isinstance(data[-1],tuple) and \
+ # not data[-1][0]:
+ # print "Received search reference: "
+ # pprint.pprint(data[-1][1])
+ # data.pop() # remove the last non-entry element
+
+ return objtype, [Entry(x) for x in data]
+ else:
+ raise TypeError("unknown data type %s returned by result"
%
+ type(data))
+ else:
+ return objtype, data
+ elif name.startswith('add'):
+ # the first arg is self
+ # the second and third arg are the dn and the data to send
+ # We need to convert the Entry into the format used by
+ # python-ldap
+ ent = args[0]
+ if isinstance(ent, Entry):
+ return f(ent.dn, ent.toTupleList(), *args[2:])
+ else:
+ return f(*args, **kwargs)
+ else:
+ return f(*args, **kwargs)
+ return inner
+
+
+def pid_exists(pid):
+ if pid <= 0:
+ return False
+ try:
+ os.kill(pid, 0)
+ except OSError as err:
+ if err.errno == errno.ESRCH:
+ return False
+ elif err.errno == errno.EPERM:
+ return True
+ else:
+ raise
+ return True
+
+def pid_from_file(pidfile):
+ pid = None
+ try:
+ with open(pidfile, 'rb') as f:
+ for line in f.readlines():
+ try:
+ pid = int(line.strip())
+ break
+ except ValueError:
+ continue
+ except IOError:
+ pass
+ return pid
+
+def _ds_shutil_copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
+ ignore_dangling_symlinks=False):
+ """Recursively copy a directory tree.
+ This is taken from /usr/lib64/python3.5/shutil.py, but removes the
+ copystat function at the end. Why? Because in a container without
+ privileges, we don't have access to set xattr. But copystat attempts to
+ set the xattr when we are root, which causes the copy to fail. Remove it!
+ """
+ names = os.listdir(src)
+ if ignore is not None:
+ ignored_names = ignore(src, names)
+ else:
+ ignored_names = set()
+
+ os.makedirs(dst)
+ errors = []
+ for name in names:
+ if name in ignored_names:
+ continue
+ srcname = os.path.join(src, name)
+ dstname = os.path.join(dst, name)
+ try:
+ if os.path.islink(srcname):
+ linkto = os.readlink(srcname)
+ if symlinks:
+ # We can't just leave it to `copy_function` because legacy
+ # code with a custom `copy_function` may rely on copytree
+ # doing the right thing.
+ os.symlink(linkto, dstname)
+ copystat(srcname, dstname, follow_symlinks=not symlinks)
+ else:
+ # ignore dangling symlink if the flag is on
+ if not os.path.exists(linkto) and ignore_dangling_symlinks:
+ continue
+ # otherwise let the copy occurs. copy2 will raise an error
+ if os.path.isdir(srcname):
+ _ds_shutil_copytree(srcname, dstname, symlinks, ignore,
+ copy_function)
+ else:
+ copy_function(srcname, dstname)
+ elif os.path.isdir(srcname):
+ _ds_shutil_copytree(srcname, dstname, symlinks, ignore, copy_function)
+ else:
+ # Will raise a SpecialFileError for unsupported file types
+ copy_function(srcname, dstname)
+ # catch the Error from the recursive copytree so that we can
+ # continue with other files
+ except Error as err:
+ errors.extend(err.args[0])
+ except OSError as why:
+ errors.append((srcname, dstname, str(why)))
+ return dst
+
+
+class DirSrv(SimpleLDAPObject, object):
+
+ def __initPart2(self):
+ """Initialize the DirSrv structure filling various fields, like:
+ self.errlog -> nsslapd-errorlog
+ self.accesslog -> nsslapd-accesslog
+ self.auditlog -> nsslapd-auditlog
+ self.confdir -> nsslapd-certdir
+ self.inst -> equivalent to self.serverid
+ self.sroot/self.inst -> nsslapd-instancedir
+ self.dbdir -> dirname(nsslapd-directory)
+ self.bakdir -> nsslapd-bakdir
+ self.ldifdir -> nsslapd-ldifdir
+
+ @param - self
+
+ @return - None
+
+ @raise ldap.LDAPError - if failure during initialization
+
+ """
+ self.errlog = self.ds_paths.error_log
+ self.accesslog = self.ds_paths.access_log
+ self.auditlog = self.ds_paths.audit_log
+ self.confdir = self.ds_paths.config_dir
+ self.schemadir = self.ds_paths.schema_dir
+ self.bakdir = self.ds_paths.backup_dir
+ self.ldifdir = self.ds_paths.ldif_dir
+ self.instdir = self.ds_paths.inst_dir
+ self.dbdir = self.ds_paths.db_dir
+ self.changelogdir = os.path.join(os.path.dirname(self.dbdir),
DEFAULT_CHANGELOG_DB)
+
+ def rebind(self):
+ """Reconnect to the DS
+
+ @raise ldap.CONFIDENTIALITY_REQUIRED - missing TLS:
+ """
+ if hasattr(ldap, 'PYLDAP_VERSION') and MAJOR >= 3:
+ super(DirSrv, self).__init__(uri, bytes_mode=False, trace_level=TRACE_LEVEL)
+ else:
+ super(DirSrv, self).__init__(uri, trace_level=TRACE_LEVEL)
+ # self.start_tls_s()
+ self.simple_bind_s(ensure_str(self.binddn), self.bindpw)
+
+ def __add_brookers__(self):
+ from lib389.config import Config
+ from lib389.aci import Aci
+ from lib389.nss_ssl import NssSsl
+ from lib389.config import RSA
+ from lib389.config import Encryption
+ from lib389.dirsrv_log import DirsrvAccessLog, DirsrvErrorLog
+ from lib389.ldclt import Ldclt
+ from lib389.mappingTree import MappingTrees
+ from lib389.mappingTree import MappingTreeLegacy as MappingTree
+ from lib389.backend import Backends
+ from lib389.backend import BackendLegacy as Backend
+ from lib389.suffix import Suffix
+ from lib389.replica import ReplicaLegacy as Replica
+ from lib389.replica import Replicas
+ from lib389.changelog import Changelog
+ from lib389.agreement import Agreement
+ from lib389.schema import SchemaLegacy as Schema
+ from lib389.plugins import Plugins
+ from lib389.tasks import Tasks
+ from lib389.index import IndexLegacy as Index
+ from lib389.monitor import Monitor, MonitorLDBM
+ from lib389.rootdse import RootDSE
+
+ # Need updating
+ self.agreement = Agreement(self)
+ self.replica = Replica(self)
+ self.changelog = Changelog(self)
+ self.backend = Backend(self)
+ self.config = Config(self)
+ self.index = Index(self)
+ self.mappingtree = MappingTree(self)
+ self.suffix = Suffix(self)
+ self.schema = Schema(self)
+ self.plugins = Plugins(self)
+ self.tasks = Tasks(self)
+ # Do we have a certdb path?
+ # if MAJOR < 3:
+ self.monitor = Monitor(self)
+ self.monitorldbm = MonitorLDBM(self)
+ self.rootdse = RootDSE(self)
+ self.backends = Backends(self)
+ self.mappingtrees = MappingTrees(self)
+ self.replicas = Replicas(self)
+ self.aci = Aci(self)
+ self.nss_ssl = NssSsl(self)
+ self.rsa = RSA(self)
+ self.encryption = Encryption(self)
+ self.ds_access_log = DirsrvAccessLog(self)
+ self.ds_error_log = DirsrvErrorLog(self)
+ self.ldclt = Ldclt(self)
+
+ def __init__(self, verbose=False, timeout=10):
+ """
+ This method does various initialization of DirSrv object:
+ parameters:
+ - 'state' to DIRSRV_STATE_INIT
+ - 'verbose' flag for debug purpose
+ - 'log' so that the use the module defined logger
+
+ wrap the methods.
+
+ - from SimpleLDAPObject
+ - from agreement, backends, suffix...
+
+ It just create a DirSrv object. To use it the user will likely do
+ the following additional steps:
+ - allocate
+ - create
+ - open
+ """
+
+ self.state = DIRSRV_STATE_INIT
+ self.verbose = verbose
+ if self.verbose:
+ log.setLevel(logging.DEBUG)
+ else:
+ log.setLevel(logging.INFO)
+ self.log = log
+
+ self.timeout = timeout
+ self.confdir = None
+
+ self.ds_paths = Paths(instance=self)
+
+ # Reset the args (py.test reuses the args_instance for each test case)
+ args_instance[SER_DEPLOYED_DIR] = os.environ.get('PREFIX',
self.ds_paths.prefix)
+ args_instance[SER_BACKUP_INST_DIR] = os.environ.get('BACKUPDIR',
DEFAULT_BACKUPDIR)
+ args_instance[SER_ROOT_DN] = DN_DM
+ args_instance[SER_ROOT_PW] = PW_DM
+ args_instance[SER_HOST] = LOCALHOST
+ args_instance[SER_PORT] = DEFAULT_PORT
+ args_instance[SER_SECURE_PORT] = None
+ args_instance[SER_SERVERID_PROP] = None # "template"
+ args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX
+ args_instance[SER_USER_ID] = None
+ args_instance[SER_GROUP_ID] = None
+ args_instance[SER_REALM] = None
+ args_instance[SER_INST_SCRIPTS_ENABLED] = None
+
+ # We allocate a "default" prefix here which allows an un-allocate or
+ # un-instantiated DirSrv
+ # instance to be able to do an an instance discovery. For example:
+ # ds = lib389.DirSrv()
+ # ds.list(all=True)
+ self.prefix = args_instance[SER_DEPLOYED_DIR]
+ self.containerised = False
+
+ self.__wrapmethods()
+ self.__add_brookers__()
+
+ def __str__(self):
+ """XXX and in SSL case?"""
+ return self.host + ":" + str(self.port)
+
+ # Should there be an extra boolean to this function to determine to use
+ # ldapi or not? Or does the settings presence indicate intent?
+ def allocate(self, args):
+ '''
+ Initialize a DirSrv object according to the provided args.
+ The final state -> DIRSRV_STATE_ALLOCATED
+ @param args - dictionary that contains the DirSrv properties
+ properties are
+ SER_SERVERID_PROP: used for offline op
+ (create/delete/backup/start/stop..) -> slapd-<serverid>
+ SER_HOST: hostname [LOCALHOST]
+ SER_PORT: normal ldap port [DEFAULT_PORT]
+ SER_SECURE_PORT: secure ldap port
+ SER_ROOT_DN: root DN [DN_DM]
+ SER_ROOT_PW: password of root DN [PW_DM]
+ SER_USER_ID: user id of the create instance [DEFAULT_USER]
+ SER_GROUP_ID: group id of the create instance [SER_USER_ID]
+ SER_DEPLOYED_DIR: directory where 389-ds is deployed
+ SER_BACKUP_INST_DIR: directory where instances will be
+ backed up
+
+ @return None
+
+ @raise ValueError - if missing mandatory properties or invalid
+ state of DirSrv
+ '''
+ if self.state != DIRSRV_STATE_INIT and \
+ self.state != DIRSRV_STATE_ALLOCATED:
+ raise ValueError("invalid state for calling allocate: %s" %
+ self.state)
+
+ if SER_SERVERID_PROP not in args:
+ self.log.debug('SER_SERVERID_PROP not provided, assuming non-local
instance')
+ # The lack of this value basically rules it out in most cases
+ self.isLocal = False
+ self.ds_paths = Paths(instance=self)
+ else:
+ self.ds_paths = Paths(args[SER_SERVERID_PROP], instance=self)
+
+
+ # Do we have ldapi settings?
+ # Do we really need .strip() on this?
+ self.ldapi_enabled = args.get(SER_LDAPI_ENABLED, 'off')
+ self.ldapi_socket = args.get(SER_LDAPI_SOCKET, None)
+ self.host = None
+ self.ldapuri = None
+ self.sslport = None
+ self.port = None
+ self.inst_scripts = args.get(SER_INST_SCRIPTS_ENABLED, None)
+ # Or do we have tcp / ip settings?
+ if self.ldapi_enabled == 'on' and self.ldapi_socket is not None:
+ self.ldapi_autobind = args.get(SER_LDAPI_AUTOBIND, 'off')
+ self.isLocal = True
+ if self.verbose:
+ self.log.info("Allocate %s with %s" % (self.__class__,
self.ldapi_socket))
+ elif args.get(SER_LDAP_URL, None) is not None:
+ self.ldapuri = args.get(SER_LDAP_URL)
+ if self.verbose:
+ self.log.info("Allocate %s with %s" % (self.__class__,
self.ldapuri))
+ else:
+ # Settings from args of server attributes
+ self.strict_hostname = args.get(SER_STRICT_HOSTNAME_CHECKING, False)
+ if self.strict_hostname is True:
+ self.host = args.get(SER_HOST, LOCALHOST)
+ if self.host == LOCALHOST:
+ DirSrvTools.testLocalhost()
+ else:
+ # Make sure our name is in hosts
+ DirSrvTools.searchHostsFile(self.host, None)
+ else:
+ self.host = args.get(SER_HOST, LOCALHOST_SHORT)
+ self.port = args.get(SER_PORT, DEFAULT_PORT)
+ self.sslport = args.get(SER_SECURE_PORT)
+ self.isLocal = isLocalHost(self.host)
+ if self.verbose:
+ self.log.info("Allocate %s with %s:%s" % (self.__class__,
self.host, (self.sslport or self.port)))
+
+ self.binddn = args.get(SER_ROOT_DN, DN_DM)
+ self.bindpw = args.get(SER_ROOT_PW, PW_DM)
+ self.creation_suffix = args.get(SER_CREATION_SUFFIX, DEFAULT_SUFFIX)
+ # These settings are only needed on a local connection.
+ if self.isLocal:
+ self.userid = args.get(SER_USER_ID)
+ if not self.userid:
+ if os.getuid() == 0:
+ # as root run as default user
+ self.userid = DEFAULT_USER
+ else:
+ self.userid = pwd.getpwuid(os.getuid())[0]
+
+ # Settings from args of server attributes
+ self.serverid = args.get(SER_SERVERID_PROP, None)
+ self.groupid = args.get(SER_GROUP_ID, self.userid)
+ self.backupdir = args.get(SER_BACKUP_INST_DIR, DEFAULT_BACKUPDIR)
+ # Allocate from the args, or use our env, or use /
+ if args.get(SER_DEPLOYED_DIR, self.prefix) is not None:
+ self.prefix = args.get(SER_DEPLOYED_DIR, self.prefix)
+ self.realm = args.get(SER_REALM, None)
+ if self.realm is not None:
+ self.krb5_realm = MitKrb5(realm=self.realm, debug=self.verbose)
+
+ # Those variables needs to be revisited (sroot for 64 bits)
+ # self.sroot = os.path.join(self.prefix, "lib/dirsrv")
+ # self.errlog = os.path.join(self.prefix,
+ # "var/log/dirsrv/slapd-%s/errors" %
self.serverid)
+
+ # additional settings
+ self.suffixes = {}
+ self.agmt = {}
+
+ self.state = DIRSRV_STATE_ALLOCATED
+ if self.verbose:
+ self.log.info("Allocate %s with %s:%s" % (self.__class__,
+ self.host,
+ (self.sslport or
+ self.port)))
+
+ def openConnection(self, *args, **kwargs):
+ """
+ Open a new connection to our LDAP server
+ *IMPORTANT*
+ This is different to re-opening on the same dirsrv, as bugs in pyldap
+ mean that ldap.set_option doesn't take effect! You need to use this
+ to allow some of the start TLS options to work!
+ """
+ server = DirSrv(verbose=self.verbose)
+ args_instance[SER_HOST] = self.host
+ args_instance[SER_PORT] = self.port
+ if self.sslport is not None:
+ args_instance[SER_SECURE_PORT] = self.sslport
+ args_instance[SER_SERVERID_PROP] = self.serverid
+ args_standalone = args_instance.copy()
+ server.allocate(args_standalone)
+ server.open(*args, **kwargs)
+
+ return server
+
+ def list(self, all=False, serverid=None):
+ """
+ Returns a list dictionary. For a created instance that is on the
+ local file system (e.g. <prefix>/etc/dirsrv/slapd-*), it exists
+ a file describing its properties
+ (environment): <prefix>/etc/sysconfig/dirsrv-<serverid> or
+ $HOME/.dirsrv/dirsv-<serverid>
+ A dictionary is created with the following properties:
+ CONF_SERVER_DIR
+ CONF_SERVERBIN_DIR
+ CONF_CONFIG_DIR
+ CONF_INST_DIR
+ CONF_RUN_DIR
+ CONF_DS_ROOT
+ CONF_PRODUCT_NAME
+ If all=True it builds a list of dictionaries for all created
+ instances. Else (default), the list will only contain the
+ dictionary of the calling instance
+
+ @param all - True or False . default is [False]
+
+ @param instance - The name of the instance to retrieve or None for
+ the current instance
+
+ @return - list of dictionaries, each of them containing instance
+ properities
+
+ @raise IOError - if the file containing the properties is not
+ foundable or readable
+ """
+ def test_and_set(prop, propname, variable, value):
+ '''
+ If variable is 'propname' it adds to
+ 'prop' dictionary the propname:value
+ '''
+ if variable == propname:
+ prop[propname] = value
+ return 1
+ return 0
+
+ def _parse_configfile(filename=None, serverid=None):
+ '''
+ This method read 'filename' and build a dictionary with
+ CONF_* properties
+ '''
+
+ if not filename:
+ raise IOError('filename is mandatory')
+ if not os.path.isfile(filename) or \
+ not os.access(filename, os.R_OK):
+ raise IOError('invalid file name or rights: %s' % filename)
+
+ prop = {}
+ prop[CONF_SERVER_ID] = serverid
+ prop[SER_SERVERID_PROP] = serverid
+ prop[SER_DEPLOYED_DIR] = self.prefix
+ myfile = open(filename, 'r')
+ for line in myfile:
+ # retrieve the value in line::
+ # <PROPNAME>=<string> [';' export
<PROPNAME>]
+
+ # skip comment lines
+ if line.startswith('#'):
+ continue
+
+ # skip lines without assignment
+ if '=' not in line:
+ continue
+ value = line.split(';', 1)[0]
+
+ # skip lines without assignment
+ if '=' not in value:
+ continue
+
+ variable = value.split('=', 1)[0]
+ value = value.split('=', 1)[1]
+ value = value.strip(' \t\n')
+ for property in (CONF_SERVER_DIR,
+ CONF_SERVERBIN_DIR,
+ CONF_CONFIG_DIR,
+ CONF_INST_DIR,
+ CONF_RUN_DIR,
+ CONF_DS_ROOT,
+ CONF_PRODUCT_NAME):
+ if test_and_set(prop, property, variable, value):
+ break
+
+ # Now, we have passed the sysconfig environment file.
+ # read in and parse the dse.ldif to determine our SER_* values.
+ # probably should use path join?
+ dsefile = '%s/dse.ldif' % prop[CONF_CONFIG_DIR]
+ if os.path.exists(dsefile):
+ ldifconn = LDIFConn(dsefile)
+ configentry = ldifconn.get(DN_CONFIG)
+ for key in args_dse_keys:
+ prop[key] = configentry.getValue(args_dse_keys[key])
+ # SER_HOST (host) nsslapd-localhost
+ # SER_PORT (port) nsslapd-port
+ # SER_SECURE_PORT (sslport) nsslapd-secureport
+ # SER_ROOT_DN (binddn) nsslapd-rootdn
+ # SER_ROOT_PW (bindpw) We can't do this
+ # SER_CREATION_SUFFIX (creation_suffix)
+ # nsslapd-defaultnamingcontext
+ # SER_USER_ID (userid) nsslapd-localuser
+ # SER_SERVERID_PROP (serverid) Already have this
+ # SER_GROUP_ID (groupid) ???
+ # SER_DEPLOYED_DIR (prefix) Already provided to for
+ # discovery
+ # SER_BACKUP_INST_DIR (backupdir) nsslapd-bakdir <<-- maybe?
+ # We need to convert these two to int
+ # because other routines get confused if we don't
+ for intkey in [SER_PORT, SER_SECURE_PORT]:
+ if prop[intkey] is not None:
+ prop[intkey] = int(prop[intkey])
+ return prop
+
+ def search_dir(instances, pattern, stop_value=None):
+ '''
+ It search all the files matching pattern.
+ It there is not stop_value, it adds the properties found in
+ each file to the 'instances'
+ Else it searches the specific stop_value (instance's serverid)
+ to add only its properties in the 'instances'
+
+ @param instances - list of dictionary containing the instances
+ properties
+ @param pattern - pattern to find the files containing the
+ properties
+ @param stop_value - serverid value if we are looking only for
+ one specific instance
+
+ @return True or False - If stop_value is None it returns False.
+ If stop_value is specified, it returns
+ True if it added the property
+ dictionary in instances. Or False if it
+ did not find it.
+ '''
+ added = False
+ for instance in glob.glob(pattern):
+ serverid = os.path.basename(instance)[len(DEFAULT_ENV_HEAD):]
+
+ # skip removed instance and admin server entry
+ if '.removed' in serverid or 'dirsrv-admin' in instance:
+ continue
+
+ # it is found, store its properties in the list
+ if stop_value:
+ if stop_value == serverid:
+ instances.append(_parse_configfile(instance, serverid))
+ added = True
+ break
+ else:
+ # this is not the searched value, continue
+ continue
+ else:
+ # we are not looking for a specific value, just add it
+ instances.append(_parse_configfile(instance, serverid))
+
+ return added
+
+ # Retrieves all instances under '/etc/sysconfig' and
'/etc/dirsrv'
+
+ # Instances/Environment are
+ #
+ # file: /etc/sysconfig/dirsrv-<serverid> (env)
+ # inst: /etc/dirsrv/slapd-<serverid> (conf)
+ #
+ # or
+ #
+ # file: $HOME/.dirsrv/dirsrv-<serverid> (env)
+ # inst: <prefix>/etc/dirsrv/slapd-<serverid> (conf)
+ #
+
+ # Don't need a default value now since it's set in init.
+ prefix = self.prefix
+ if serverid is None and hasattr(self, 'serverid'):
+ serverid = self.serverid
+
+ # first identify the directories we will scan
+ sysconfig_head = self.ds_paths.initconfig_dir
+ privconfig_head = os.path.join(os.getenv('HOME'), ENV_LOCAL_DIR)
+ if not os.path.isdir(sysconfig_head):
+ privconfig_head = None
+ if self.verbose:
+ self.log.info("dir (sys) : %s" % sysconfig_head)
+ if privconfig_head and self.verbose:
+ self.log.info("dir (priv): %s" % privconfig_head)
+
+ # list of the found instances
+ instances = []
+
+ # now prepare the list of instances properties
+ if not all:
+ # easy case we just look for the current instance
+
+ # we have two location to retrieve the self.serverid
+ # privconfig_head and sysconfig_head
+
+ # first check the private repository
+ if privconfig_head:
+ pattern = "%s*" % os.path.join(privconfig_head,
+ DEFAULT_ENV_HEAD)
+ found = search_dir(instances, pattern, serverid)
+ if self.verbose and len(instances) > 0:
+ self.log.info("List from %s" % privconfig_head)
+ for instance in instances:
+ self.log.info("list instance %r\n" % instance)
+ if found:
+ assert len(instances) == 1
+ else:
+ assert len(instances) == 0
+ else:
+ found = False
+
+ # second, if not already found, search the system repository
+ if not found:
+ pattern = "%s*" % os.path.join(sysconfig_head,
+ DEFAULT_ENV_HEAD)
+ search_dir(instances, pattern, serverid)
+ if self.verbose and len(instances) > 0:
+ self.log.info("List from %s" % privconfig_head)
+ for instance in instances:
+ self.log.info("list instance %r\n" % instance)
+
+ else:
+ # all instances must be retrieved
+ if privconfig_head:
+ pattern = "%s*" % os.path.join(privconfig_head,
+ DEFAULT_ENV_HEAD)
+ search_dir(instances, pattern)
+ if self.verbose and len(instances) > 0:
+ self.log.info("List from %s" % privconfig_head)
+ for instance in instances:
+ self.log.info("list instance %r\n" % instance)
+
+ pattern = "%s*" % os.path.join(sysconfig_head, DEFAULT_ENV_HEAD)
+ search_dir(instances, pattern)
+ if self.verbose and len(instances) > 0:
+ self.log.info("List from %s" % privconfig_head)
+ for instance in instances:
+ self.log.info("list instance %r\n" % instance)
+
+ return instances
+
+ def _createDirsrv(self):
+ """Create a new instance of directory server
+
+ @param self - containing the set properties
+
+ SER_HOST (host)
+ SER_PORT (port)
+ SER_SECURE_PORT (sslport)
+ SER_ROOT_DN (binddn)
+ SER_ROOT_PW (bindpw)
+ SER_CREATION_SUFFIX (creation_suffix)
+ SER_USER_ID (userid)
+ SER_SERVERID_PROP (serverid)
+ SER_GROUP_ID (groupid)
+ SER_DEPLOYED_DIR (prefix)
+ SER_BACKUP_INST_DIR (backupdir)
+ SER_REALM (krb5_realm)
+
+ @return None
+
+ @raise None
+
+ }
+ """
+
+ DirSrvTools.lib389User(user=DEFAULT_USER)
+ prog = os.path.join(self.ds_paths.sbin_dir, CMD_PATH_SETUP_DS)
+
+ if not os.path.isfile(prog):
+ log.error("Can't find file: %r, removing extension" % prog)
+ prog = prog[:-3]
+
+ # Create and extract a service keytab
+ args = {SER_HOST: self.host,
+ SER_PORT: self.port,
+ SER_SECURE_PORT: self.sslport,
+ SER_ROOT_DN: self.binddn,
+ SER_ROOT_PW: self.bindpw,
+ SER_CREATION_SUFFIX: self.creation_suffix,
+ SER_USER_ID: self.userid,
+ SER_SERVERID_PROP: self.serverid,
+ SER_GROUP_ID: self.groupid,
+ SER_DEPLOYED_DIR: self.prefix,
+ SER_BACKUP_INST_DIR: self.backupdir,
+ SER_STRICT_HOSTNAME_CHECKING: self.strict_hostname}
+
+ if self.inst_scripts is not None:
+ args[SER_INST_SCRIPTS_ENABLED] = self.inst_scripts
+
+ content = formatInfData(args)
+ result = DirSrvTools.runInfProg(prog, content, self.verbose,
+ prefix=self.prefix)
+ if result != 0:
+ raise Exception('Failed to run setup-ds.pl')
+ if self.realm is not None:
+ # This may conflict in some tests, we may need to use /etc/host
+ # aliases or we may need to use server id
+ self.krb5_realm.create_principal(principal='ldap/%s' % self.host)
+ ktab = '%s/ldap.keytab' % (self.ds_paths.config_dir)
+ self.krb5_realm.create_keytab(principal='ldap/%s' % self.host,
keytab=ktab)
+ with open('%s/dirsrv-%s' % (self.ds_paths.initconfig_dir,
self.serverid), 'a') as sfile:
+ sfile.write("\nKRB5_KTNAME=%s/etc/dirsrv/slapd-%s/"
+ "ldap.keytab\nexport KRB5_KTNAME\n" %
+ (self.prefix, self.serverid))
+ self.restart()
+
+ # Restart the instance
+
+ def _createPythonDirsrv(self, version):
+ """
+ Create a new dirsrv instance based on the new python installer, rather
+ than setup-ds.pl
+
+ version represents the config default and sample entry version to use.
+ """
+ from lib389.instance.setup import SetupDs
+ from lib389.instance.options import General2Base, Slapd2Base
+ # Import the new setup ds library.
+ sds = SetupDs(verbose=self.verbose, dryrun=False, log=self.log)
+ # Configure the options.
+ general_options = General2Base(self.log)
+ general_options.set('strict_host_checking', False)
+ general_options.verify()
+ general = general_options.collect()
+
+ slapd_options = Slapd2Base(self.log)
+ slapd_options.set('instance_name', self.serverid)
+ slapd_options.set('port', self.port)
+ slapd_options.set('secure_port', self.sslport)
+ slapd_options.set('root_password', self.bindpw)
+ slapd_options.set('root_dn', self.binddn)
+ slapd_options.set('defaults', version)
+
+ slapd_options.verify()
+ slapd = slapd_options.collect()
+
+ # In order to work by "default" for tests, we need to create a
backend.
+ userroot = {
+ 'cn': 'userRoot',
+ 'nsslapd-suffix': self.creation_suffix,
+ BACKEND_SAMPLE_ENTRIES: version,
+ }
+ backends = [userroot,]
+
+ # Go!
+ sds.create_from_args(general, slapd, backends, None)
+ if self.realm is not None:
+ # This may conflict in some tests, we may need to use /etc/host
+ # aliases or we may need to use server id
+ self.krb5_realm.create_principal(principal='ldap/%s' % self.host)
+ ktab = '%s/ldap.keytab' % (self.ds_paths.config_dir)
+ self.krb5_realm.create_keytab(principal='ldap/%s' % self.host,
keytab=ktab)
+ with open('%s/dirsrv-%s' % (self.ds_paths.initconfig_dir,
self.serverid), 'a') as sfile:
+ sfile.write("\nKRB5_KTNAME=%s/etc/dirsrv/slapd-%s/"
+ "ldap.keytab\nexport KRB5_KTNAME\n" %
+ (self.prefix, self.serverid))
+ self.restart()
+
+ def create(self, pyinstall=False, version=INSTALL_LATEST_CONFIG):
+ """
+ Creates an instance with the parameters sets in dirsrv
+ The state change from DIRSRV_STATE_ALLOCATED ->
+ DIRSRV_STATE_OFFLINE
+
+ @param - self
+
+ @return - None
+
+ @raise ValueError - if 'serverid' is missing or if it exist an
+ instance with the same 'serverid'
+ """
+ # check that DirSrv was in DIRSRV_STATE_ALLOCATED state
+ self.log.debug("Server is in state %s" % self.state)
+ if self.state != DIRSRV_STATE_ALLOCATED:
+ raise ValueError("invalid state for calling create: %s" %
+ self.state)
+
+ if self.exists():
+ raise ValueError("Error it already exists the instance (%s)" %
+ self.list()[0][CONF_INST_DIR])
+
+ if not self.serverid:
+ raise ValueError("SER_SERVERID_PROP is missing, " +
+ "it is required to create an instance")
+ # Time to create the instance and retrieve the effective sroot
+ if (not self.ds_paths.perl_enabled or pyinstall):
+ self._createPythonDirsrv(version)
+ else:
+ self._createDirsrv()
+
+ # Retrieve sroot from the sys/priv config file
+ assert(self.exists())
+ self.sroot = self.list()[0][CONF_SERVER_DIR]
+
+ # Now the instance is created but DirSrv is not yet connected to it
+ self.state = DIRSRV_STATE_OFFLINE
+
+ def _deleteDirsrv(self):
+ '''
+ Deletes the instance with the parameters sets in dirsrv
+ The state changes -> DIRSRV_STATE_ALLOCATED
+
+ @param self
+
+ @return None
+
+ @raise None
+ '''
+
+ # Grab all the instances now, before we potentially remove the last one
+ insts = self.list(all=True)
+
+ if self.state == DIRSRV_STATE_ONLINE:
+ self.close()
+
+ if not self.exists():
+ raise ValueError("Error can not find instance %s[%s:%d]" %
+ (self.serverid, self.host, self.port))
+
+ # Now time to remove the instance
+ prog = os.path.join(self.ds_paths.sbin_dir, CMD_PATH_REMOVE_DS)
+ if (not self.prefix or self.prefix == '/') and os.geteuid() != 0:
+ raise ValueError("Error: without prefix deployment it is required to be
root user")
+ cmd = "%s -i %s%s" % (prog, DEFAULT_INST_HEAD, self.serverid)
+ self.log.debug("running: %s " % cmd)
+ try:
+ os.system(cmd)
+ except:
+ log.exception("error executing %r" % cmd)
+
+ # If this was the last instance being deleted, remove the DEFAULT_USER
+ # if lib389 created the default user
+ if os.getuid() == 0:
+ # Only the root user could of added the entry
+ if len(insts) == 1:
+ # No more instances (this was the last one)
+ if pwd.getpwnam(DEFAULT_USER).pw_gecos == DEFAULT_USER_COMMENT:
+ # We created this user, so we will delete it
+ cmd = ['/usr/sbin/userdel', DEFAULT_USER]
+ try:
+ subprocess.call(cmd)
+ except subprocess.CalledProcessError as e:
+ log.exception('Failed to delete default user ' +
+ '(%s): error %s' % (DEFAULT_USER,
+ e.output))
+
+ self.state = DIRSRV_STATE_ALLOCATED
+
+ def delete(self, pyinstall=False):
+ # Time to create the instance and retrieve the effective sroot
+ if (not self.ds_paths.perl_enabled or pyinstall):
+ from lib389.instance.remove import remove_ds_instance
+ remove_ds_instance(self)
+ else:
+ self._deleteDirsrv()
+ # Now, we are still an allocated ds object so we can be re-installed
+ self.state = DIRSRV_STATE_ALLOCATED
+
+ def open(self, saslmethod=None, sasltoken=None, certdir=None, starttls=False,
connOnly=False, reqcert=ldap.OPT_X_TLS_HARD,
+ usercert=None, userkey=None):
+ '''
+ It opens a ldap bound connection to dirsrv so that online
+ administrative tasks are possible. It binds with the binddn
+ property, then it initializes various fields from DirSrv
+ (via __initPart2)
+
+ The state changes -> DIRSRV_STATE_ONLINE
+
+ @param self
+ @param saslmethod - None, or GSSAPI
+ @param sasltoken - The ldap.sasl token type to bind with.
+ @param certdir - Certificate directory for TLS
+ @return None
+
+ @raise LDAPError
+ '''
+
+ ##################
+ # WARNING: While you have a python ldap connection open some settings like
+ # ldap.set_option MAY NOT WORK AS YOU EXPECT.
+ # There are cases (especially CACERT/USERCERTS) where when one connection
+ # is open set_option SILENTLY fails!!!!
+ #
+ # You MAY need to set post_open=False in your DirSrv start/restart instance!
+ ##################
+
+ uri = self.toLDAPURL()
+
+ if certdir is None and self.isLocal:
+ certdir = self.get_cert_dir()
+ log.debug("Using dirsrv ca certificate %s" % certdir)
+
+ if userkey is not None:
+ # Note this sets LDAP.OPT not SELF. Because once self has opened
+ # it can NOT change opts AT ALL.
+ ldap.set_option(ldap.OPT_X_TLS_KEYFILE, ensure_str(userkey))
+ log.debug("Using user private key %s" % userkey)
+ if usercert is not None:
+ # Note this sets LDAP.OPT not SELF. Because once self has opened
+ # it can NOT change opts AT ALL.
+ ldap.set_option(ldap.OPT_X_TLS_CERTFILE, ensure_str(usercert))
+ log.debug("Using user certificate %s" % usercert)
+
+ if certdir is not None:
+ """
+ We have a certificate directory, so lets start up TLS negotiations
+ """
+ # Note this sets LDAP.OPT not SELF. Because once self has opened
+ # it can NOT change opts AT ALL.
+ ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, ensure_str(certdir))
+ log.debug("Using external ca certificate %s" % certdir)
+
+ if certdir or starttls:
+ try:
+ # Note this sets LDAP.OPT not SELF. Because once self has opened
+ # it can NOT change opts on reused (ie restart)
+ ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, reqcert)
+ log.debug("Using certificate policy %s" % reqcert)
+ log.debug("ldap.OPT_X_TLS_REQUIRE_CERT = %s" % reqcert)
+ except ldap.LDAPError as e:
+ log.fatal('TLS negotiation failed: %s' % str(e))
+ raise e
+
+ ## NOW INIT THIS. This MUST be after all the ldap.OPT set above,
+ # so that we inherit the settings correctly!!!!
+ if self.verbose:
+ self.log.info('open(): Connecting to uri %s' % uri)
+ if hasattr(ldap, 'PYLDAP_VERSION') and MAJOR >= 3:
+ super(DirSrv, self).__init__(uri, bytes_mode=False, trace_level=TRACE_LEVEL)
+ else:
+ super(DirSrv, self).__init__(uri, trace_level=TRACE_LEVEL)
+
+ if starttls and not uri.startswith('ldaps'):
+ self.start_tls_s()
+
+ if saslmethod and saslmethod.lower() == 'gssapi':
+ """
+ Perform kerberos/gssapi authentication
+ """
+ try:
+ sasl_auth = ldap.sasl.gssapi("")
+ self.sasl_interactive_bind_s("", sasl_auth)
+ except ldap.LOCAL_ERROR as e:
+ # No Ticket - ultimately invalid credentials
+ log.debug("Error: No Ticket (%s)" % str(e))
+ raise ldap.INVALID_CREDENTIALS
+ except ldap.LDAPError as e:
+ log.debug("SASL/GSSAPI Bind Failed: %s" % str(e))
+ raise e
+
+ elif saslmethod == 'EXTERNAL':
+ # Do nothing.
+ sasl_auth = ldap.sasl.external()
+ self.sasl_interactive_bind_s("", sasl_auth)
+ elif saslmethod and sasltoken is not None:
+ # Just pass the sasltoken in!
+ self.sasl_interactive_bind_s("", sasltoken)
+ elif saslmethod:
+ # Unknown or unsupported method
+ log.debug('Unsupported SASL method: %s' % saslmethod)
+ raise ldap.UNWILLING_TO_PERFORM
+
+ elif self.can_autobind():
+ # Connect via ldapi, and autobind.
+ # do nothing: the bind is complete.
+ if self.verbose:
+ log.info("open(): Using root autobind ...")
+ sasl_auth = ldap.sasl.external()
+ self.sasl_interactive_bind_s("", sasl_auth)
+
+ else:
+ """
+ Do a simple bind
+ """
+ try:
+ self.simple_bind_s(ensure_str(self.binddn), self.bindpw)
+ except ldap.SERVER_DOWN as e:
+ # TODO add server info in exception
+ log.debug("Cannot connect to %r" % uri)
+ raise e
+ except ldap.LDAPError as e:
+ log.debug("Error: Failed to authenticate: %s", str(e))
+ raise e
+
+ """
+ Authenticated, now finish the initialization
+ """
+ if self.verbose:
+ log.info("open(): bound as %s" % self.binddn)
+ if not connOnly:
+ self.__initPart2()
+ self.state = DIRSRV_STATE_ONLINE
+
+ def close(self):
+ '''
+ It closes connection to dirsrv. Online administrative tasks are no
+ longer possible.
+
+ The state changes from DIRSRV_STATE_ONLINE -> DIRSRV_STATE_OFFLINE
+
+ @param self
+
+ @return None
+ @raise ValueError - if the instance has not the right state
+ '''
+ # check that DirSrv was in DIRSRV_STATE_ONLINE state
+ if self.state == DIRSRV_STATE_ONLINE:
+ # Don't raise an error. Just move the state and return
+ self.unbind_s()
+
+ self.state = DIRSRV_STATE_OFFLINE
+
+ def start(self, timeout=120, post_open=True):
+ '''
+ It starts an instance and rebind it. Its final state after rebind
+ (open) is DIRSRV_STATE_ONLINE
+
+ @param self
+ @param timeout (in sec) to wait for successful start
+
+ @return None
+
+ @raise None
+ '''
+
+ if self.status() is True:
+ return
+
+ if self.with_systemd() and not self.containerised:
+ # Do systemd things here ...
+ subprocess.check_call(["/usr/bin/systemctl",
+ "start",
+ "dirsrv@%s" % self.serverid])
+ else:
+ # Start the process.
+ # Wait for it to terminate
+ # This means the server is probably ready to go ....
+ env = {}
+ if self.has_asan():
+ log.error("NOTICE: Starting instance with ASAN options")
+ log.error("This is probably not what you want. Please contact
support.")
+ log.error("ASAN options will be copied from your environment")
+ env['ASAN_SYMBOLIZER_PATH'] =
"/usr/bin/llvm-symbolizer"
+ env['ASAN_OPTIONS'] = "symbolize=1 detect_deadlocks=1
log_path=%s/ns-slapd-%s.asan" % (self.ds_paths.run_dir, self.serverid)
+ env.update(os.environ)
+ subprocess.check_call(["%s/ns-slapd" % self.get_sbin_dir(),
+ "-D",
+ self.ds_paths.config_dir,
+ "-i",
+ self.ds_paths.pid_file], env=env)
+ count = timeout
+ pid = pid_from_file(self.ds_paths.pid_file)
+ while (pid is None) and count > 0:
+ count -= 1
+ time.sleep(1)
+ pid = pid_from_file(self.ds_paths.pid_file)
+ if pid == 0 or pid is None:
+ raise ValueError
+ # Wait
+ while not pid_exists(pid) and count > 0:
+ # It looks like DS changes the value in here at some point ...
+ # It's probably a DS bug, but if we "keep checking" the
file, eventually
+ # we get the main server pid, and it's ready to go.
+ pid = pid_from_file(self.ds_paths.pid_file)
+ time.sleep(1)
+ count -= 1
+ if not pid_exists(pid):
+ raise Exception("Failed to start DS")
+ if post_open:
+ self.open()
+
+ def stop(self, timeout=120):
+ '''
+ It stops an instance.
+ It changes the state -> DIRSRV_STATE_OFFLINE
+
+ @param self
+ @param timeout (in sec) to wait for successful stop
+
+ @return None
+
+ @raise None
+ '''
+ if self.status() is False:
+ return
+
+ if self.with_systemd() and not self.containerised:
+ # Do systemd things here ...
+ subprocess.check_call(["/usr/bin/systemctl",
+ "stop",
+ "dirsrv@%s" % self.serverid])
+ else:
+ # TODO: Make the pid path in the files things
+ # TODO: use the status call instead!!!!
+ count = timeout
+ pid = pid_from_file(self.ds_paths.pid_file)
+ if pid == 0 or pid is None:
+ raise ValueError
+ os.kill(pid, signal.SIGTERM)
+ # Wait
+ while pid_exists(pid) and count > 0:
+ time.sleep(1)
+ count -= 1
+ if pid_exists(pid):
+ os.kill(pid, signal.SIGKILL)
+ self.state = DIRSRV_STATE_OFFLINE
+
+ def status(self):
+ """
+ Determine if an instance is running or not.
+
+ Will update the self.state parameter.
+ """
+ if self.with_systemd() and not self.containerised:
+ # Do systemd things here ...
+ rc = subprocess.call(["/usr/bin/systemctl",
+ "is-active", "--quiet",
+ "dirsrv@%s" % self.serverid])
+ if rc == 0:
+ return True
+ # This .... probably will mess something up
+ # self.state = DIRSRV_STATE_RUNNING
+ self.state = DIRSRV_STATE_OFFLINE
+ return False
+ else:
+ # TODO: Make the pid path in the files things
+ # TODO: use the status call instead!!!!
+ pid = pid_from_file(self.ds_paths.pid_file)
+ if pid is None:
+ # No pidfile yet ...
+ self.state = DIRSRV_STATE_OFFLINE
+ return False
+ if pid == 0:
+ self.state = DIRSRV_STATE_OFFLINE
+ raise ValueError
+ # Wait
+ if not pid_exists(pid):
+ self.state = DIRSRV_STATE_OFFLINE
+ return False
+ return True
+
+ def restart(self, timeout=120, post_open=True):
+ '''
+ It restarts an instance and rebind it. Its final state after rebind
+ (open) is DIRSRV_STATE_ONLINE.
+
+ @param self
+ @param timeout (in sec) to wait for successful stop
+
+ @return None
+
+ @raise None
+ '''
+ self.stop(timeout)
+ time.sleep(1)
+ self.start(timeout, post_open)
+
+ def _infoBackupFS(self):
+ """
+ Return the information to retrieve the backup file of a given
+ instance
+ It returns:
+ - Directory name containing the backup
+ (e.g. /tmp/slapd-standalone.bck)
+ - The pattern of the backup files
+ (e.g. /tmp/slapd-standalone.bck/backup*.tar.gz)
+ """
+ backup_dir = "%s/slapd-%s.bck" % (self.backupdir, self.serverid)
+ backup_pattern = os.path.join(backup_dir, "backup*.tar.gz")
+ return backup_dir, backup_pattern
+
+ def clearBackupFS(self, backup_file=None):
+ """
+ Remove a backup_file or all backup up of a given instance
+
+ @param backup_file - optional
+
+ @return None
+
+ @raise None
+ """
+ if backup_file:
+ if os.path.isfile(backup_file):
+ try:
+ os.remove(backup_file)
+ except:
+ log.info("clearBackupFS: fail to remove %s" % backup_file)
+ pass
+ else:
+ backup_dir, backup_pattern = self._infoBackupFS()
+ list_backup_files = glob.glob(backup_pattern)
+ for f in list_backup_files:
+ try:
+ os.remove(f)
+ except:
+ log.info("clearBackupFS: fail to remove %s" % backup_file)
+ pass
+
+ def checkBackupFS(self):
+ """
+ If it exits a backup file, it returns it
+ else it returns None
+
+ @param None
+
+ @return file name of the first backup. None if there is no backup
+
+ @raise None
+ """
+
+ backup_dir, backup_pattern = self._infoBackupFS()
+ list_backup_files = glob.glob(backup_pattern)
+ if not list_backup_files:
+ return None
+ else:
+ # returns the first found backup
+ return list_backup_files[0]
+
+ def backupFS(self):
+ """
+ Saves the files of an instance under:
+ /tmp/slapd-<instance_name>.bck/backup_HHMMSS.tar.gz
+ and return the archive file name.
+ If it already exists a such file, it assums it is a valid backup
+ and returns its name
+
+ self.sroot : root of the instance (e.g. /usr/lib64/dirsrv)
+ self.inst : instance name
+ (e.g. standalone for /etc/dirsrv/slapd-standalone)
+ self.confdir : root of the instance config (e.g. /etc/dirsrv)
+ self.dbdir: directory where is stored the database
+ (e.g. /var/lib/dirsrv/slapd-standalone/db)
+ self.changelogdir: directory where is stored the changelog
+ (e.g. /var/lib/dirsrv/slapd-master/changelogdb)
+
+ @param None
+
+ @return file name of the backup
+
+ @raise none
+ """
+
+ # First check it if already exists a backup file
+ backup_dir, backup_pattern = self._infoBackupFS()
+ if not os.path.exists(backup_dir):
+ os.makedirs(backup_dir)
+ # make the backup directory accessible for anybody so that any user can
+ # run the tests even if it existed a backup created by somebody else
+ os.chmod(backup_dir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
+
+ # Forget this: Just make a new backup!
+ # backup_file = self.checkBackupFS()
+ # if backup_file:
+ # return backup_file
+
+ # goes under the directory where the DS is deployed
+ listFilesToBackup = []
+ here = os.getcwd()
+ if self.prefix:
+ os.chdir("%s/" % self.prefix)
+ prefix_pattern = "%s/" % self.prefix
+ else:
+ os.chdir("/")
+ prefix_pattern = None
+
+ # build the list of directories to scan
+ # THIS MUST BE FIXED, No guarantee of sroot!
+ instroot = "%s/slapd-%s" % (self.sroot, self.serverid)
+ ldir = [instroot]
+ if hasattr(self, 'confdir'):
+ ldir.append(self.confdir)
+ if hasattr(self, 'dbdir'):
+ ldir.append(self.dbdir)
+ if hasattr(self, 'changelogdir'):
+ ldir.append(self.changelogdir)
+ if hasattr(self, 'errlog'):
+ ldir.append(os.path.dirname(self.errlog))
+ if hasattr(self, 'accesslog') and \
+ os.path.dirname(self.accesslog) not in ldir:
+ ldir.append(os.path.dirname(self.accesslog))
+ if hasattr(self, 'auditlog') and os.path.dirname(self.auditlog) \
+ not in ldir:
+ ldir.append(os.path.dirname(self.auditlog))
+
+ # now scan the directory list to find the files to backup
+ for dirToBackup in ldir:
+ for root, dirs, files in os.walk(dirToBackup):
+
+ for b_dir in dirs:
+ name = os.path.join(root, b_dir)
+ log.debug("backupFS b_dir = %s (%s) [name=%s]" %
+ (b_dir, self.prefix, name))
+ if prefix_pattern:
+ name = re.sub(prefix_pattern, '', name)
+
+ if os.path.isdir(name):
+ listFilesToBackup.append(name)
+ log.debug("backupFS add = %s (%s)" %
+ (name, self.prefix))
+
+ for file in files:
+ name = os.path.join(root, file)
+ if prefix_pattern:
+ name = re.sub(prefix_pattern, '', name)
+
+ if os.path.isfile(name):
+ listFilesToBackup.append(name)
+ log.debug("backupFS add = %s (%s)" %
+ (name, self.prefix))
+
+ # create the archive
+ name = "backup_%s_%s.tar.gz" % (self.serverid,
time.strftime("%m%d%Y_%H%M%S"))
+ backup_file = os.path.join(backup_dir, name)
+ tar = tarfile.open(backup_file, "w:gz")
+
+ for name in listFilesToBackup:
+ tar.add(name)
+ tar.close()
+ log.info("backupFS: archive done : %s" % backup_file)
+
+ # return to the directory where we were
+ os.chdir(here)
+
+ return backup_file
+
+ def restoreFS(self, backup_file):
+ """
+ Restore a directory from a backup file
+
+ @param backup_file - file name of the backup
+
+ @return None
+
+ @raise ValueError - if backup_file invalid file name
+ """
+
+ # First check the archive exists
+ if backup_file is None:
+ log.warning("Unable to restore the instance (missing backup)")
+ raise ValueError("Unable to restore the instance (missing
backup)")
+ if not os.path.isfile(backup_file):
+ log.warning("Unable to restore the instance (%s is not a file)" %
+ backup_file)
+ raise ValueError("Unable to restore the instance " +
+ "(%s is not a file)" % backup_file)
+
+ #
+ # Second do some clean up
+ #
+
+ # previous db (it may exists new db files not in the backup)
+ log.debug("restoreFS: remove subtree %s/*" %
os.path.dirname(self.dbdir))
+ for root, dirs, files in os.walk(os.path.dirname(self.dbdir)):
+ for d in dirs:
+ if d not in ("bak", "ldif"):
+ log.debug("restoreFS: before restore remove directory" +
+ " %s/%s" % (root, d))
+ shutil.rmtree("%s/%s" % (root, d))
+
+ # previous error/access logs
+ log.debug("restoreFS: remove error logs %s" % self.errlog)
+ for f in glob.glob("%s*" % self.errlog):
+ log.debug("restoreFS: before restore remove file %s" % (f))
+ os.remove(f)
+ log.debug("restoreFS: remove access logs %s" % self.accesslog)
+ for f in glob.glob("%s*" % self.accesslog):
+ log.debug("restoreFS: before restore remove file %s" % (f))
+ os.remove(f)
+ log.debug("restoreFS: remove audit logs %s" % self.accesslog)
+ for f in glob.glob("%s*" % self.auditlog):
+ log.debug("restoreFS: before restore remove file %s" % (f))
+ os.remove(f)
+
+ # Then restore from the directory where DS was deployed
+ here = os.getcwd()
+ if self.prefix:
+ prefix_pattern = "%s/" % self.prefix
+ os.chdir(prefix_pattern)
+ else:
+ prefix_pattern = "/"
+ os.chdir(prefix_pattern)
+
+ tar = tarfile.open(backup_file)
+ for member in tar.getmembers():
+ if os.path.isfile(member.name):
+ #
+ # restore only writable files
+ # It could be a bad idea and preferably restore all.
+ # Now it will be easy to enhance that function.
+ if os.access(member.name, os.W_OK):
+ log.debug("restoreFS: restored %s" % member.name)
+ tar.extract(member.name)
+ else:
+ log.debug("restoreFS: not restored %s (no write access)" %
+ member.name)
+ else:
+ log.debug("restoreFS: restored %s" % member.name)
+ tar.extract(member.name)
+
+ tar.close()
+
+ #
+ # Now be safe, triggers a recovery at restart
+ #
+ guardian_file = os.path.join(self.dbdir, "guardian")
+ if os.path.isfile(guardian_file):
+ try:
+ log.debug("restoreFS: remove %s" % guardian_file)
+ os.remove(guardian_file)
+ except:
+ log.warning("restoreFS: fail to remove %s" % guardian_file)
+ pass
+
+ os.chdir(here)
+
+ def exists(self):
+ '''
+ Check if an instance exists.
+ It checks that both exists:
+ - configuration directory
(<prefix>/etc/dirsrv/slapd-<servid>)
+ - environment file (/etc/sysconfig/dirsrv-<serverid> or
+ $HOME/.dirsrv/dirsrv-<serverid>)
+
+ @param None
+
+ @return True of False if the instance exists or not
+
+ @raise None
+
+ '''
+ return len(self.list()) == 1
+
+ def toLDAPURL(self):
+ """Return the uri ldap[s]://host:[ssl]port."""
+ host = self.host
+ if self.ldapi_enabled == 'on' and self.ldapi_socket is not None:
+ return "ldapi://%s" %
(ldapurl.ldapUrlEscape(ensure_str(ldapi_socket)))
+ elif self.ldapuri:
+ return self.ldapuri
+ elif self.sslport:
+ return "ldaps://%s:%d/" % (ensure_str(host), self.sslport)
+ else:
+ return "ldap://%s:%d/" % (ensure_str(host), self.port)
+
+ def can_autobind(self):
+ """Check if autobind/LDAPI is enabled."""
+ return self.ldapi_enabled == 'on' and self.ldapi_socket is not None and
self.ldapi_autobind == 'on'
+
+ def getServerId(self):
+ """Return the server identifier."""
+ return self.serverid
+
+ def get_ldif_dir(self):
+ """Return the server instance ldif directory."""
+ return self.ds_paths.ldif_dir
+
+ def get_bak_dir(self):
+ """Return the server instance ldif directory."""
+ return self.ds_paths.backup_dir
+
+ def get_local_state_dir(self):
+ return self.ds_paths.local_state_dir
+
+ def get_config_dir(self):
+ return self.ds_paths.config_dir
+
+ def get_cert_dir(self):
+ return self.ds_paths.cert_dir
+
+ def get_sysconf_dir(self):
+ return self.ds_paths.sysconf_dir
+
+ def get_initconfig_dir(self):
+ return self.ds_paths.initconfig_dir
+
+ def get_sbin_dir(self):
+ return self.ds_paths.sbin_dir
+
+ def get_bin_dir(self):
+ return self.ds_paths.bin_dir
+
+ def get_plugin_dir(self):
+ return self.ds_paths.plugin_dir
+
+ def get_tmp_dir(self):
+ return self.ds_paths.tmp_dir
+
+ def has_asan(self):
+ return self.ds_paths.asan_enabled
+
+ def with_systemd(self):
+ return self.ds_paths.with_systemd
+
+ #
+ # Get entries
+ #
+ def getEntry(self, *args, **kwargs):
+ """Wrapper around SimpleLDAPObject.search. It is common to just
get
+ one entry.
+ @param - entry dn
+ @param - search scope, in ldap.SCOPE_BASE (default),
+ ldap.SCOPE_SUB, ldap.SCOPE_ONE
+ @param filterstr - filterstr, default '(objectClass=*)' from
+ SimpleLDAPObject
+ @param attrlist - list of attributes to retrieve. eg ['cn',
'uid']
+ @oaram attrsonly - default None from SimpleLDAPObject
+ eg. getEntry(dn, scope, filter, attributes)
+
+ XXX This cannot return None
+ """
+ if self.verbose:
+ log.debug("Retrieving entry with %r" % [args])
+ if len(args) == 1 and 'scope' not in kwargs:
+ args += (ldap.SCOPE_BASE, )
+
+ res = self.search(*args, **kwargs)
+ restype, obj = self.result(res)
+ # TODO: why not test restype?
+ if not obj:
+ raise NoSuchEntryError("no such entry for %r" % [args])
+
+ if self.verbose:
+ log.info("Retrieved entry %s" % obj)
+ if isinstance(obj, Entry):
+ return obj
+ else: # assume list/tuple
+ if obj[0] is None:
+ raise NoSuchEntryError("Entry is None")
+ return obj[0]
+
+ def _test_entry(self, dn, scope=ldap.SCOPE_BASE):
+ try:
+ entry = self.getEntry(dn, scope)
+ log.info("Found entry %s" % entry)
+ return entry
+ except NoSuchEntryError:
+ log.exception("Entry %s was added successfully, but I cannot " +
+ "search it" % dn)
+ raise MissingEntryError("Entry %s was added successfully, but " +
+ "I cannot search it" % dn)
+
+ def __wrapmethods(self):
+ """This wraps all methods of SimpleLDAPObject, so that we can
intercept
+ the methods that deal with entries. Instead of using a raw list of
+ tuples of lists of hashes of arrays as the entry object, we want to
+ wrap entries in an Entry class that provides some useful
methods"""
+ for name in dir(self.__class__.__bases__[0]):
+ attr = getattr(self, name)
+ if isinstance(attr, collections.Callable):
+ setattr(self, name, wrapper(attr, name))
+
+ def addLDIF(self, input_file, cont=False):
+ class LDIFAdder(ldif.LDIFParser):
+ def __init__(self, input_file, conn, cont=False,
+ ignored_attr_types=None, max_entries=0,
+ process_url_schemes=None
+ ):
+ myfile = input_file
+ if isinstance(input_file, six.string_types):
+ myfile = open(input_file, "r")
+ self.conn = conn
+ self.cont = cont
+ ldif.LDIFParser.__init__(self, myfile, ignored_attr_types,
+ max_entries, process_url_schemes)
+ self.parse()
+ if isinstance(input_file, six.string_types):
+ myfile.close()
+
+ def handle(self, dn, entry):
+ if not dn:
+ dn = ''
+ newentry = Entry((dn, entry))
+ try:
+ self.conn.add_s(newentry)
+ except ldap.LDAPError as e:
+ if not self.cont:
+ raise e
+ log.exception("Error: could not add entry %s" % dn)
+
+ adder = LDIFAdder(input_file, self, cont)
+
+ def getDBStats(self, suffix, bename=''):
+ if bename:
+ dn = ','.join(("cn=monitor,cn=%s" % bename, DN_LDBM))
+ else:
+ entries_backend = self.backend.list(suffix=suffix)
+ dn = "cn=monitor," + entries_backend[0].dn
+ dbmondn = "cn=monitor," + DN_LDBM
+ dbdbdn = "cn=database,cn=monitor," + DN_LDBM
+ try:
+ # entrycache and dncache stats
+ ent = self.getEntry(dn, ldap.SCOPE_BASE)
+ monent = self.getEntry(dbmondn, ldap.SCOPE_BASE)
+ dbdbent = self.getEntry(dbdbdn, ldap.SCOPE_BASE)
+ ret = "cache available ratio count unitsize\n"
+ mecs = ent.maxentrycachesize or "0"
+ cecs = ent.currententrycachesize or "0"
+ rem = int(mecs) - int(cecs)
+ ratio = ent.entrycachehitratio or "0"
+ ratio = int(ratio)
+ count = ent.currententrycachecount or "0"
+ count = int(count)
+ if count:
+ size = int(cecs) / count
+ else:
+ size = 0
+ ret += "entry % 11d % 3d % 8d % 5d" % (rem, ratio, count, size)
+ if ent.maxdncachesize:
+ mdcs = ent.maxdncachesize or "0"
+ cdcs = ent.currentdncachesize or "0"
+ rem = int(mdcs) - int(cdcs)
+ dct = ent.dncachetries or "0"
+ tries = int(dct)
+ if tries:
+ ratio = (100 * int(ent.dncachehits)) / tries
+ else:
+ ratio = 0
+ count = ent.currentdncachecount or "0"
+ count = int(count)
+ if count:
+ size = int(cdcs) / count
+ else:
+ size = 0
+ ret += "\ndn % 11d % 3d % 8d % 5d" % (
+ rem, ratio, count, size)
+
+ if ent.hasAttr('entrycache-hashtables'):
+ ret += "\n\n" + ent.getValue('entrycache-hashtables')
+
+ # global db stats
+ ret += "\n\nglobal db stats"
+ dbattrs = ('dbcachehits dbcachetries dbcachehitratio ' +
+ 'dbcachepagein dbcachepageout dbcacheroevict ' +
+ 'dbcacherwevict'.split(' '))
+ cols = {'dbcachehits': [len('cachehits'),
'cachehits'],
+ 'dbcachetries': [10, 'cachetries'],
+ 'dbcachehitratio': [5, 'ratio'],
+ 'dbcachepagein': [6, 'pagein'],
+ 'dbcachepageout': [7, 'pageout'],
+ 'dbcacheroevict': [7, 'roevict'],
+ 'dbcacherwevict': [7, 'rwevict']}
+ dbrec = {}
+ for attr, vals in monent.iterAttrs():
+ if attr.startswith('dbcache'):
+ val = vals[0]
+ dbrec[attr] = val
+ vallen = len(val)
+ if vallen > cols[attr][0]:
+ cols[attr][0] = vallen
+ # construct the format string based on the field widths
+ fmtstr = ''
+ ret += "\n"
+ for attr in dbattrs:
+ fmtstr += ' %%(%s)%ds' % (attr, cols[attr][0])
+ ret += ' %*s' % tuple(cols[attr])
+ ret += "\n" + (fmtstr % dbrec)
+
+ # other db stats
+ skips = {'nsslapd-db-cache-hit': 'nsslapd-db-cache-hit',
+ 'nsslapd-db-cache-try': 'nsslapd-db-cache-try',
+ 'nsslapd-db-page-write-rate':
+ 'nsslapd-db-page-write-rate',
+ 'nsslapd-db-page-read-rate':
'nsslapd-db-page-read-rate',
+ 'nsslapd-db-page-ro-evict-rate':
+ 'nsslapd-db-page-ro-evict-rate',
+ 'nsslapd-db-page-rw-evict-rate':
+ 'nsslapd-db-page-rw-evict-rate'}
+
+ hline = '' # header line
+ vline = '' # val line
+ for attr, vals in dbdbent.iterAttrs():
+ if attr in skips:
+ continue
+ if attr.startswith('nsslapd-db-'):
+ short = attr.replace('nsslapd-db-', '')
+ val = vals[0]
+ width = max(len(short), len(val))
+ if len(hline) + width > 70:
+ ret += "\n" + hline + "\n" + vline
+ hline = vline = ''
+ hline += ' %*s' % (width, short)
+ vline += ' %*s' % (width, val)
+
+ # per file db stats
+ ret += "\n\nper file stats"
+ # key is number
+ # data is dict - key is attr name without the number -
+ # val is the attr val
+ dbrec = {}
+ dbattrs = ['dbfilename', 'dbfilecachehit',
+ 'dbfilecachemiss', 'dbfilepagein',
'dbfilepageout']
+ # cols maps dbattr name to column header and width
+ cols = {'dbfilename': [len('dbfilename'),
'dbfilename'],
+ 'dbfilecachehit': [9, 'cachehits'],
+ 'dbfilecachemiss': [11, 'cachemisses'],
+ 'dbfilepagein': [6, 'pagein'],
+ 'dbfilepageout': [7, 'pageout']}
+ for attr, vals in ent.iterAttrs():
+ match = RE_DBMONATTR.match(attr)
+ if match:
+ name = match.group(1)
+ num = match.group(2)
+ val = vals[0]
+ if name == 'dbfilename':
+ val = val.split('/')[-1]
+ dbrec.setdefault(num, {})[name] = val
+ vallen = len(val)
+ if vallen > cols[name][0]:
+ cols[name][0] = vallen
+ match = RE_DBMONATTRSUN.match(attr)
+ if match:
+ name = match.group(1)
+ if name == 'entrycache':
+ continue
+ num = match.group(2)
+ val = vals[0]
+ if name == 'dbfilename':
+ val = val.split('/')[-1]
+ dbrec.setdefault(num, {})[name] = val
+ vallen = len(val)
+ if vallen > cols[name][0]:
+ cols[name][0] = vallen
+ # construct the format string based on the field widths
+ fmtstr = ''
+ ret += "\n"
+ for attr in dbattrs:
+ fmtstr += ' %%(%s)%ds' % (attr, cols[attr][0])
+ ret += ' %*s' % tuple(cols[attr])
+ for dbf in six.itervalues(dbrec):
+ ret += "\n" + (fmtstr % dbf)
+ return ret
+ except Exception as e:
+ print ("caught exception", str(e))
+ return ''
+
+ def waitForEntry(self, dn, timeout=7200, attr='', quiet=True):
+ scope = ldap.SCOPE_BASE
+ filt = "(objectclass=*)"
+ attrlist = []
+ if attr:
+ filt = "(%s=*)" % attr
+ attrlist.append(attr)
+ timeout += int(time.time())
+
+ if isinstance(dn, Entry):
+ dn = dn.dn
+
+ # wait for entry and/or attr to show up
+ if not quiet:
+ sys.stdout.write("Waiting for %s %s:%s " % (self, dn, attr))
+ sys.stdout.flush()
+ entry = None
+ while not entry and int(time.time()) < timeout:
+ try:
+ entry = self.getEntry(dn, scope, filt, attrlist)
+ except NoSuchEntryError:
+ pass # found entry, but no attr
+ except ldap.NO_SUCH_OBJECT:
+ pass # no entry yet
+ except ldap.LDAPError as e: # badness
+ print("\nError reading entry", dn, e)
+ break
+ if not entry:
+ if not quiet:
+ sys.stdout.write(".")
+ sys.stdout.flush()
+ time.sleep(1)
+
+ if not entry and int(time.time()) > timeout:
+ print("\nwaitForEntry timeout for %s for %s" % (self, dn))
+ elif entry:
+ if not quiet:
+ print("\nThe waited for entry is:", entry)
+ else:
+ print("\nError: could not read entry %s from %s" % (dn, self))
+
+ return entry
+
+ def setupChainingIntermediate(self):
+ confdn = ','.join(("cn=config", DN_CHAIN))
+ try:
+ self.modify_s(confdn, [(ldap.MOD_ADD, 'nsTransmittedControl',
+ ['2.16.840.1.113730.3.4.12',
+ '1.3.6.1.4.1.1466.29539.12'])])
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ log.error("chaining backend config already has the required
ctrls")
+
+ def setupChainingMux(self, suffix, isIntermediate, binddn, bindpw, urls):
+ self.addSuffix(suffix, binddn, bindpw, urls)
+ if isIntermediate:
+ self.setupChainingIntermediate()
+
+ def setupChainingFarm(self, suffix, binddn, bindpw):
+ # step 1 - create the bind dn to use as the proxy
+ self.setupBindDN(binddn, bindpw)
+ self.addSuffix(suffix) # step 2 - create the suffix
+ # step 3 - add the proxy ACI to the suffix
+ try:
+ acival = ("(targetattr = \"*\")(version 3.0; acl
\"Proxied " +
+ "authorization for database links\"; allow (proxy) "
+
+ "userdn = \"ldap:///%s\";)" % binddn)
+ self.modify_s(suffix, [(ldap.MOD_ADD, 'aci', [acival])])
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ log.error("proxy aci already exists in suffix %s for %s" % (
+ suffix, binddn))
+
+ def setupChaining(self, to, suffix, isIntermediate):
+ """Setup chaining from self to to - self is the mux, to is the
farm
+ if isIntermediate is set, this server will chain requests from another
+ server to to
+ """
+ bindcn = "chaining user"
+ binddn = "cn=%s,cn=config" % bindcn
+ bindpw = "chaining"
+
+ to.setupChainingFarm(suffix, binddn, bindpw)
+ self.setupChainingMux(
+ suffix, isIntermediate, binddn, bindpw, to.toLDAPURL())
+
+ def enableChainOnUpdate(self, suffix, bename):
+ # first, get the mapping tree entry to modify
+ mtent = self.mappingtree.list(suffix=suffix)
+ dn = mtent.dn
+
+ # next, get the path of the replication plugin
+ e_plugin = self.getEntry(
+ "cn=Multimaster Replication Plugin,cn=plugins,cn=config",
+ attrlist=['nsslapd-pluginPath'])
+ path = e_plugin.getValue('nsslapd-pluginPath')
+
+ mod = [(ldap.MOD_REPLACE, MT_PROPNAME_TO_ATTRNAME[MT_STATE],
+ MT_STATE_VAL_BACKEND),
+ (ldap.MOD_ADD, MT_PROPNAME_TO_ATTRNAME[MT_BACKEND], bename),
+ (ldap.MOD_ADD, MT_PROPNAME_TO_ATTRNAME[MT_CHAIN_PATH], path),
+ (ldap.MOD_ADD, MT_PROPNAME_TO_ATTRNAME[MT_CHAIN_FCT],
+ MT_CHAIN_UPDATE_VAL_ON_UPDATE)]
+
+ try:
+ self.modify_s(dn, mod)
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ print("chainOnUpdate already enabled for %s" % suffix)
+
+ def setupConsumerChainOnUpdate(self, suffix, isIntermediate, binddn,
+ bindpw, urls, beargs=None):
+ beargs = beargs or {}
+ # suffix should already exist
+ # we need to create a chaining backend
+ if 'nsCheckLocalACI' not in beargs:
+ beargs['nsCheckLocalACI'] = 'on' # enable local db aci
eval.
+ chainbe = self.setupBackend(suffix, binddn, bindpw, urls, beargs)
+ # do the stuff for intermediate chains
+ if isIntermediate:
+ self.setupChainingIntermediate()
+ # enable the chain on update
+ return self.enableChainOnUpdate(suffix, chainbe)
+
+ def setupBindDN(self, binddn, bindpw, attrs=None):
+ """ Return - eventually creating - a person entry with the given
dn
+ and pwd.
+
+ binddn can be a lib389.Entry
+ """
+ try:
+ assert binddn
+ if isinstance(binddn, Entry):
+ assert binddn.dn
+ binddn = binddn.dn
+ except AssertionError:
+ raise AssertionError("Error: entry dn should be set!" % binddn)
+
+ ent = Entry(binddn)
+ ent.setValues('objectclass', "top", "person")
+ ent.setValues('userpassword', bindpw)
+ ent.setValues('sn', "bind dn pseudo user")
+ ent.setValues('cn', "bind dn pseudo user")
+
+ # support for uid
+ attribute, value = binddn.split(",")[0].split("=", 1)
+ if attribute == 'uid':
+ ent.setValues('objectclass', "top", "person",
'inetOrgPerson')
+ ent.setValues('uid', value)
+
+ if attrs:
+ ent.update(attrs)
+
+ try:
+ self.add_s(ent)
+ except ldap.ALREADY_EXISTS:
+ log.warn("Entry %s already exists" % binddn)
+
+ try:
+ entry = self._test_entry(binddn, ldap.SCOPE_BASE)
+ return entry
+ except MissingEntryError:
+ log.exception("This entry should exist!")
+ raise
+
+ def setupWinSyncAgmt(self, args, entry):
+ if 'winsync' not in args:
+ return
+
+ suffix = args['suffix']
+ entry.setValues("objectclass",
"nsDSWindowsReplicationAgreement")
+ entry.setValues("nsds7WindowsReplicaSubtree",
+ args.get("win_subtree",
+ "cn=users," + suffix))
+ entry.setValues("nsds7DirectoryReplicaSubtree",
+ args.get("ds_subtree",
+ "ou=People," + suffix))
+ entry.setValues(
+ "nsds7NewWinUserSyncEnabled", args.get('newwinusers',
'true'))
+ entry.setValues(
+ "nsds7NewWinGroupSyncEnabled", args.get('newwingroups',
'true'))
+ windomain = ''
+ if 'windomain' in args:
+ windomain = args['windomain']
+ else:
+ windomain = '.'.join(ldap.explode_dn(suffix, 1))
+ entry.setValues("nsds7WindowsDomain", windomain)
+ if 'interval' in args:
+ entry.setValues("winSyncInterval", args['interval'])
+ if 'onewaysync' in args:
+ if args['onewaysync'].lower() == 'fromwindows' or \
+ args['onewaysync'].lower() == 'towindows':
+ entry.setValues("oneWaySync", args['onewaysync'])
+ else:
+ raise Exception("Error: invalid value %s for oneWaySync: " +
+ "must be fromWindows or toWindows"
+ % args['onewaysync'])
+
+ # args - DirSrv consumer (repoth), suffix, binddn, bindpw, timeout
+ # also need an auto_init argument
+ def createAgreement(self, consumer, args, cn_format=r'meTo_%s:%s',
+ description_format=r'me to %s:%s'):
+ """Create (and return) a replication agreement from self to
consumer.
+ - self is the supplier,
+ - consumer is a DirSrv object (consumer can be a master)
+ - cn_format - use this string to format the agreement name
+
+ consumer:
+ * a DirSrv object if chaining
+ * an object with attributes: host, port, sslport, __str__
+
+ args = {
+ 'suffix': "dc=example,dc=com",
+ 'binddn': "cn=replrepl,cn=config",
+ 'bindpw': "replrepl",
+ 'bindmethod': 'simple',
+ 'log' : True.
+ 'timeout': 120
+ }
+
+ self.suffixes is of the form {
+ 'o=suffix1': 'ldaps://consumer.example.com:636',
+ 'o=suffix2': 'ldap://consumer.example.net:3890'
+ }
+ """
+ suffix = args['suffix']
+ if not suffix:
+ # This is a mandatory parameter of the command... it fails
+ log.warning("createAgreement: suffix is missing")
+ return None
+
+ # get the RA binddn
+ binddn = args.get('binddn')
+ if not binddn:
+ binddn = defaultProperties.get(REPLICATION_BIND_DN, None)
+ if not binddn:
+ # weird, internal error we do not retrieve the default
+ # replication bind DN this replica agreement will fail
+ # to update the consumer until the property will be set
+ log.warning("createAgreement: binddn not provided and " +
+ "default value unavailable")
+ pass
+
+ # get the RA binddn password
+ bindpw = args.get('bindpw')
+ if not bindpw:
+ bindpw = defaultProperties.get(REPLICATION_BIND_PW, None)
+ if not bindpw:
+ # weird, internal error we do not retrieve the default
+ # replication bind DN password this replica agreement
+ # will fail to update the consumer until the property will be
+ # set
+ log.warning("createAgreement: bindpw not provided and " +
+ "default value unavailable")
+ pass
+
+ # get the RA bind method
+ bindmethod = args.get('bindmethod')
+ if not bindmethod:
+ bindmethod = defaultProperties.get(REPLICATION_BIND_METHOD, None)
+ if not bindmethod:
+ # weird, internal error we do not retrieve the default
+ # replication bind method this replica agreement will
+ # fail to update the consumer until the property will be set
+ log.warning("createAgreement: bindmethod not provided and " +
+ "default value unavailable")
+ pass
+
+ nsuffix = normalizeDN(suffix)
+ othhost, othport, othsslport = (
+ consumer.host, consumer.port, consumer.sslport)
+ othport = othsslport or othport
+
+ # adding agreement to previously created replica
+ # eventually setting self.suffixes dict.
+ if nsuffix not in self.suffixes:
+ replica_entries = self.replica.list(suffix)
+ if not replica_entries:
+ raise NoSuchEntryError(
+ "Error: no replica set up for suffix " + suffix)
+ replent = replica_entries[0]
+ self.suffixes[nsuffix] = {
+ 'dn': replent.dn,
+ 'type': int(replent.nsds5replicatype)
+ }
+
+ # define agreement entry
+ cn = cn_format % (othhost, othport)
+ dn_agreement = "cn=%s,%s" % (cn, self.suffixes[nsuffix]['dn'])
+ try:
+ entry = self.getEntry(dn_agreement, ldap.SCOPE_BASE)
+ except ldap.NO_SUCH_OBJECT:
+ entry = None
+ if entry:
+ log.warn("Agreement exists:", dn_agreement)
+ self.suffixes.setdefault(nsuffix, {})[str(consumer)] = dn_agreement
+ return dn_agreement
+ if (nsuffix in self.agmt) and (consumer in self.agmt[nsuffix]):
+ log.warn("Agreement exists:", dn_agreement)
+ return dn_agreement
+
+ # In a separate function in this scope?
+ entry = Entry(dn_agreement)
+ entry.update({
+ 'objectclass': ["top",
"nsds5replicationagreement"],
+ 'cn': cn,
+ 'nsds5replicahost': othhost,
+ 'nsds5replicatimeout': str(args.get('timeout', 120)),
+ 'nsds5replicabinddn': binddn,
+ 'nsds5replicacredentials': bindpw,
+ 'nsds5replicabindmethod': bindmethod,
+ 'nsds5replicaroot': nsuffix,
+ 'nsds5replicaupdateschedule': '0000-2359 0123456',
+ 'description': description_format % (othhost, othport)
+ })
+ if 'starttls' in args:
+ entry.setValues('nsds5replicatransportinfo', 'TLS')
+ entry.setValues('nsds5replicaport', str(othport))
+ elif othsslport:
+ entry.setValues('nsds5replicatransportinfo', 'SSL')
+ entry.setValues('nsds5replicaport', str(othsslport))
+ else:
+ entry.setValues('nsds5replicatransportinfo', 'LDAP')
+ entry.setValues('nsds5replicaport', str(othport))
+ if 'fractional' in args:
+ entry.setValues('nsDS5ReplicatedAttributeList',
args['fractional'])
+ if 'auto_init' in args:
+ entry.setValues('nsds5BeginReplicaRefresh', 'start')
+ if 'fractional' in args:
+ entry.setValues('nsDS5ReplicatedAttributeList',
args['fractional'])
+ if 'stripattrs' in args:
+ entry.setValues('nsds5ReplicaStripAttrs',
args['stripattrs'])
+
+ if 'winsync' in args: # state it clearly!
+ self.setupWinSyncAgmt(args, entry)
+
+ try:
+ log.debug("Adding replica agreement: [%s]" % entry)
+ self.add_s(entry)
+ except:
+ # TODO check please!
+ raise
+ entry = self.waitForEntry(dn_agreement)
+ if entry:
+ self.suffixes.setdefault(nsuffix, {})[str(consumer)] = dn_agreement
+ # More verbose but shows what's going on
+ if 'chain' in args:
+ chain_args = {
+ 'suffix': suffix,
+ 'binddn': binddn,
+ 'bindpw': bindpw
+ }
+ # Work on `self` aka producer
+ if self.suffixes[nsuffix]['type'] == MASTER_TYPE:
+ self.setupChainingFarm(**chain_args)
+ # Work on `consumer`
+ # TODO - is it really required?
+ if consumer.suffixes[nsuffix]['type'] == LEAF_TYPE:
+ chain_args.update({
+ 'isIntermediate': 0,
+ 'urls': self.toLDAPURL(),
+ 'args': args['chainargs']
+ })
+ consumer.setupConsumerChainOnUpdate(**chain_args)
+ elif consumer.suffixes[nsuffix]['type'] == HUB_TYPE:
+ chain_args.update({
+ 'isIntermediate': 1,
+ 'urls': self.toLDAPURL(),
+ 'args': args['chainargs']
+ })
+ consumer.setupConsumerChainOnUpdate(**chain_args)
+ self.agmt.setdefault(nsuffix, {})[consumer] = dn_agreement
+ return dn_agreement
+
+ # moved to Replica
+ def setupReplica(self, args):
+ """Deprecated, use replica.add
+ """
+ return self.replica.add(**args)
+
+ def startReplication_async(self, agmtdn):
+ return self.replica.start_async(agmtdn)
+
+ def checkReplInit(self, agmtdn):
+ return self.replica.check_init(agmtdn)
+
+ def waitForReplInit(self, agmtdn):
+ return self.replica.wait_init(agmtdn)
+
+ def startReplication(self, agmtdn):
+ return self.replica.start_and_wait(agmtdn)
+
+ def testReplication(self, suffix, *replicas):
+ '''
+ Make a "dummy" update on the the replicated suffix, and check
+ all the provided replicas to see if they received the update.
+
+ @param suffix - the replicated suffix we want to check
+ @param *replicas - DirSrv instance, DirSrv instance, ...
+
+ @return True of False if all servers are replicating correctly
+
+ @raise None
+ '''
+
+ test_value = ('test replication from ' + self.serverid + ' to '
+
+ replicas[0].serverid + ': ' + str(int(time.time())))
+ self.modify_s(suffix, [(ldap.MOD_REPLACE, 'description', test_value)])
+
+ for replica in replicas:
+ loop = 0
+ replicated = False
+ while loop <= 30:
+ try:
+
+ entry = replica.getEntry(suffix, ldap.SCOPE_BASE,
+ "(objectclass=*)")
+ if entry.hasValue('description', test_value):
+ replicated = True
+ break
+ except ldap.LDAPError as e:
+ log.fatal('testReplication() failed to modify (%s), error
(%s)' % (suffix, str(e)))
+ return False
+ loop += 1
+ time.sleep(2)
+ if not replicated:
+ log.fatal('Replication is not in sync with replica server (%s)'
+ % replica.serverid)
+ return False
+
+ return True
+
+ def replicaSetupAll(self, repArgs):
+ """setup everything needed to enable replication for a given
suffix.
+ 1- eventually create the suffix
+ 2- enable replication logging
+ 3- create changelog
+ 4- create replica user
+ repArgs is a dict with the following fields:
+ {
+ suffix - suffix to set up for replication (eventually create)
+ optional fields and their default values
+ bename - name of backend corresponding to suffix, otherwise
+ it will use the *first* backend found (isn't that
+ dangerous?
+ parent - parent suffix if suffix is a sub-suffix - default is
+ undef
+ ro - put database in read only mode - default is read write
+ type - replica type (MASTER_TYPE, HUB_TYPE, LEAF_TYPE) -
+ default is master
+ legacy - make this replica a legacy consumer - default is no
+
+ binddn - bind DN of the replication manager user - default is
+ REPLBINDDN
+ bindpw - bind password of the repl manager - default is
+ REPLBINDPW
+
+ log - if true, replication logging is turned on - default false
+ id - the replica ID - default is an auto incremented number
+ }
+
+ TODO: passing the repArgs as an object or as a **repArgs could be
+ a better documentation choiche
+ eg. replicaSetupAll(self, suffix, type=MASTER_TYPE,
+ log=False, ...)
+ """
+
+ repArgs.setdefault('type', MASTER_TYPE)
+ user = repArgs.get('binddn'), repArgs.get('bindpw')
+
+ # eventually create the suffix (Eg. o=userRoot)
+ # TODO should I check the addSuffix output as it doesn't raise
+ self.addSuffix(repArgs['suffix'])
+ if 'bename' not in repArgs:
+ entries_backend = self.backend.list(suffix=repArgs['suffix'])
+ # just use first one
+ repArgs['bename'] = entries_backend[0].cn
+ if repArgs.get('log', False):
+ self.enableReplLogging()
+
+ # enable changelog for master and hub
+ if repArgs['type'] != LEAF_TYPE:
+ self.replica.changelog()
+ # create replica user without timeout and expiration issues
+ try:
+ attrs = list(user)
+ attrs.append({
+ 'nsIdleTimeout': '0',
+ 'passwordExpirationTime': '20381010000000Z'
+ })
+ self.setupBindDN(*attrs)
+ except ldap.ALREADY_EXISTS:
+ log.warn("User already exists: %r " % user)
+
+ # setup replica
+ # map old style args to new style replica args
+ if repArgs['type'] == MASTER_TYPE:
+ repArgs['role'] = ReplicaRole.MASTER
+ elif repArgs['type'] == LEAF_TYPE:
+ repArgs['role'] = ReplicaRole.CONSUMER
+ else:
+ repArgs['role'] = ReplicaRole.HUB
+ repArgs['rid'] = repArgs['id']
+
+ # remove invalid arguments from replica.add
+ for invalid_arg in 'type id bename log'.split():
+ if invalid_arg in repArgs:
+ del repArgs[invalid_arg]
+
+ ret = self.replica.add(**repArgs)
+ if 'legacy' in repArgs:
+ self.setupLegacyConsumer(*user)
+
+ return ret
+
+ def subtreePwdPolicy(self, basedn, pwdpolicy, **pwdargs):
+ args = {'basedn': basedn, 'escdn': escapeDNValue(
+ normalizeDN(basedn))}
+ condn = "cn=nsPwPolicyContainer,%(basedn)s" % args
+ poldn = ("cn=cn\\=nsPwPolicyEntry\\,%(escdn)s,cn=nsPwPolicyContainer"
+
+ ",%(basedn)s" % args)
+ temdn = ("cn=cn\\=nsPwTemplateEntry\\,%(escdn)s,cn=nsPwPolicyContain"
+
+ "er,%(basedn)s" % args)
+ cosdn = "cn=nsPwPolicy_cos,%(basedn)s" % args
+ conent = Entry(condn)
+ conent.setValues('objectclass', 'nsContainer')
+ polent = Entry(poldn)
+ polent.setValues('objectclass', ['ldapsubentry',
'passwordpolicy'])
+ tement = Entry(temdn)
+ tement.setValues('objectclass', ['extensibleObject',
+ 'costemplate', 'ldapsubentry'])
+ tement.setValues('cosPriority', '1')
+ tement.setValues('pwdpolicysubentry', poldn)
+ cosent = Entry(cosdn)
+ cosent.setValues('objectclass', ['ldapsubentry',
+ 'cosSuperDefinition', 'cosPointerDefinition'])
+ cosent.setValues('cosTemplateDn', temdn)
+ cosent.setValues(
+ 'cosAttribute', 'pwdpolicysubentry default
operational-default')
+ for ent in (conent, polent, tement, cosent):
+ try:
+ self.add_s(ent)
+ if self.verbose:
+ print("created subtree pwpolicy entry", ent.dn)
+ except ldap.ALREADY_EXISTS:
+ print("subtree pwpolicy entry", ent.dn,
+ "already exists - skipping")
+ self.setPwdPolicy({'nsslapd-pwpolicy-local': 'on'})
+ self.setDNPwdPolicy(poldn, pwdpolicy, **pwdargs)
+
+ def userPwdPolicy(self, user, pwdpolicy, **pwdargs):
+ ary = ldap.explode_dn(user)
+ par = ','.join(ary[1:])
+ escuser = escapeDNValue(normalizeDN(user))
+ args = {'par': par, 'udn': user, 'escudn': escuser}
+ condn = "cn=nsPwPolicyContainer,%(par)s" % args
+ poldn = ("cn=cn\\=nsPwPolicyEntry\\,%(escudn)s,cn=nsPwPolicyCont" +
+ "ainer,%(par)s" % args)
+ conent = Entry(condn)
+ conent.setValues('objectclass', 'nsContainer')
+ polent = Entry(poldn)
+ polent.setValues('objectclass', ['ldapsubentry',
'passwordpolicy'])
+ for ent in (conent, polent):
+ try:
+ self.add_s(ent)
+ if self.verbose:
+ print("created user pwpolicy entry", ent.dn)
+ except ldap.ALREADY_EXISTS:
+ print("user pwpolicy entry", ent.dn,
+ "already exists - skipping")
+ mod = [(ldap.MOD_REPLACE, 'pwdpolicysubentry', poldn)]
+ self.modify_s(user, mod)
+ self.setPwdPolicy({'nsslapd-pwpolicy-local': 'on'})
+ self.setDNPwdPolicy(poldn, pwdpolicy, **pwdargs)
+
+ def setPwdPolicy(self, pwdpolicy, **pwdargs):
+ self.setDNPwdPolicy(DN_CONFIG, pwdpolicy, **pwdargs)
+
+ def setDNPwdPolicy(self, dn, pwdpolicy, **pwdargs):
+ """input is dict of attr/vals"""
+ mods = []
+ for (attr, val) in six.iteritems(pwdpolicy):
+ mods.append((ldap.MOD_REPLACE, attr, str(val)))
+ if pwdargs:
+ for (attr, val) in six.iteritems(pwdargs):
+ mods.append((ldap.MOD_REPLACE, attr, str(val)))
+ self.modify_s(dn, mods)
+
+ # Moved to config
+ # replaced by loglevel
+ def enableReplLogging(self):
+ """Enable logging of replication stuff
(1<<13)"""
+ val = LOG_REPLICA
+ return self.config.loglevel([val])
+
+ def disableReplLogging(self):
+ return self.config.loglevel()
+
+ def setLogLevel(self, *vals):
+ """Set nsslapd-errorlog-level and return its
value."""
+ return self.config.loglevel(vals)
+
+ def setAccessLogLevel(self, *vals):
+ """Set nsslapd-accesslog-level and return its
value."""
+ return self.config.loglevel(vals, service='access')
+
+ def setAccessLogBuffering(self, state):
+ """Set nsslapd-accesslog-logbuffering - state is True or
False"""
+ return self.config.logbuffering(state)
+
+ def configSSL(self, secport=636, secargs=None):
+ """Configure SSL support into cn=encryption,cn=config.
+
+ secargs is a dict like {
+ 'nsSSLPersonalitySSL': 'Server-Cert'
+ }
+
+ XXX moved to brooker.Config
+ """
+ return self.config.enable_ssl(secport, secargs)
+
+ def getDir(self, filename, dirtype):
+ """
+ @param filename - the name of the test script calling this function
+ @param dirtype - Either DATA_DIR and TMP_DIR are the allowed values
+ @return - absolute path of the dirsrvtests data directory, or 'None'
+ on error
+
+ Return the shared data/tmp directory relative to the ticket filename.
+ The caller should always use "__file__" as the argument to this
+ function.
+
+ Get the script name from the filename that was provided:
+
+ 'ds/dirsrvtests/tickets/ticket_#####_test.py' -->
+ 'ticket_#####_test.py'
+
+ Get the full path to the filename, and convert it to the data directory
+
+ 'ds/dirsrvtests/tickets/ticket_#####_test.py' -->
+ 'ds/dirsrvtests/data/'
+ 'ds/dirsrvtests/suites/dyanmic-plugins/dynamic-plugins_test.py'
-->
+ '/ds/dirsrvtests/data/'
+ """
+ dir_path = None
+ if os.path.exists(filename):
+ filename = os.path.abspath(filename)
+ if '/suites/' in filename:
+ idx = filename.find('/suites/')
+ elif '/tickets/' in filename:
+ idx = filename.find('/tickets/')
+ elif '/stress/' in filename:
+ idx = filename.find('/stress/')
+ else:
+ # Unknown script location
+ return None
+
+ if dirtype == TMP_DIR:
+ dir_path = filename[:idx] + '/tmp/'
+ elif dirtype == DATA_DIR:
+ dir_path = filename[:idx] + '/data/'
+ else:
+ raise ValueError("Invalid directory type (%s), acceptable" +
+ " values are DATA_DIR and TMP_DIR" % dirtype)
+
+ return dir_path
+
+ def clearTmpDir(self, filename):
+ """
+ @param filename - the name of the test script calling this function
+ @return - nothing
+
+ Clear the contents of the "tmp" dir, but leave the README file in
+ place.
+ """
+ if os.path.exists(filename):
+ filename = os.path.abspath(filename)
+ if '/suites/' in filename:
+ idx = filename.find('/suites/')
+ elif '/tickets/' in filename:
+ idx = filename.find('/tickets/')
+ else:
+ # Unknown script location
+ return
+
+ dir_path = filename[:idx] + '/tmp/'
+ if dir_path:
+ filelist = [tmpfile for tmpfile in os.listdir(dir_path)
+ if tmpfile != 'README']
+ for tmpfile in filelist:
+ tmpfile = os.path.abspath(dir_path + tmpfile)
+ if os.path.isdir(tmpfile):
+ # Remove directory and all of its contents
+ shutil.rmtree(tmpfile)
+ else:
+ os.remove(tmpfile)
+
+ return
+
+ log.fatal('Failed to clear tmp directory (%s)' % filename)
+
+ def upgrade(self, upgradeMode):
+ """
+ @param upgradeMode - the upgrade is either "online" or
"offline"
+ """
+ if upgradeMode == 'online':
+ online = True
+ else:
+ online = False
+ DirSrvTools.runUpgrade(self.prefix, online)
+
+ #
+ # The following are the functions to perform offline scripts(when the
+ # server is stopped)
+ #
+ def ldif2db(self, bename, suffixes, excludeSuffixes, encrypt,
+ import_file):
+ """
+ @param bename - The backend name of the database to import
+ @param suffixes - List/tuple of suffixes to import
+ @param excludeSuffixes - List/tuple of suffixes to exclude from import
+ @param encrypt - Perform attribute encryption
+ @param input_file - File to import: file
+ @return - True if import succeeded
+ """
+ DirSrvTools.lib389User(user=DEFAULT_USER)
+ prog = os.path.join(self.ds_paths.sbin_dir, 'ns-slapd')
+
+ if self.status():
+ log.error("ldif2db: Can not operate while directory server is
running")
+ return False
+
+ if not bename and not suffixes:
+ log.error("ldif2db: backend name or suffix missing")
+ return False
+
+ if not os.path.isfile(import_file):
+ log.error("ldif2db: Can't find file: %s" % import_file)
+ return False
+
+ cmd = [
+ prog,
+ 'ldif2db',
+ '-D', self.get_config_dir(),
+ '-i', import_file,
+ ]
+ if bename:
+ cmd.append('-n')
+ cmd.append(bename)
+ if suffixes:
+ for suffix in suffixes:
+ cmd.append('-s')
+ cmd.append(suffix)
+ if excludeSuffixes:
+ for excludeSuffix in excludeSuffixes:
+ cmd = cmd + ' -x ' + excludeSuffix
+ cmd.append('-x')
+ cmd.append(excludeSuffix)
+ if encrypt:
+ cmd.append('-E')
+
+ result = subprocess.check_output(cmd)
+ u_result = ensure_str(result)
+
+ log.debug("ldif2db output: BEGIN")
+ for line in u_result.split("\n"):
+ log.debug(line)
+ log.debug("ldif2db output: END")
+
+ return True
+
+ def db2ldif(self, bename, suffixes, excludeSuffixes, encrypt, repl_data,
+ outputfile):
+ """
+ @param bename - The backend name of the database to export
+ @param suffixes - List/tuple of suffixes to export
+ @param excludeSuffixes - List/tuple of suffixes to exclude from export
+ @param encrypt - Perform attribute encryption
+ @param repl_data - Export the replication data
+ @param outputfile - The filename for the exported LDIF
+ @return - True if export succeeded
+ """
+ DirSrvTools.lib389User(user=DEFAULT_USER)
+ prog = os.path.join(self.ds_paths.sbin_dir, 'ns-slapd')
+
+ if self.status():
+ log.error("db2ldif: Can not operate while directory server is
running")
+ return False
+
+ if not bename and not suffixes:
+ log.error("db2ldif: backend name or suffix missing")
+ return False
+
+ cmd = [
+ prog,
+ 'db2ldif',
+ '-D', self.get_config_dir()
+ ]
+ if bename:
+ cmd.append('-n')
+ cmd.append(bename)
+ if suffixes:
+ for suffix in suffixes:
+ cmd.append('-s')
+ cmd.append(suffix)
+ if excludeSuffixes:
+ for excludeSuffix in excludeSuffixes:
+ cmd = cmd + ' -x ' + excludeSuffix
+ cmd.append('-x')
+ cmd.append(excludeSuffix)
+ if encrypt:
+ cmd.append('-E')
+ if repl_data:
+ cmd.append('-r')
+ if outputfile:
+ cmd.append('-a')
+ cmd.append(outputfile)
+
+ result = subprocess.check_output(cmd)
+ u_result = ensure_str(result)
+
+ log.debug("db2ldif output: BEGIN")
+ for line in u_result.split("\n"):
+ log.debug(line)
+ log.debug("db2ldif output: END")
+
+ return True
+
+ def bak2db(self, archive_dir, bename=None):
+ """
+ @param archive_dir - The directory containing the backup
+ @param bename - The backend name to restore
+ @return - True if the restore succeeded
+ """
+ DirSrvTools.lib389User(user=DEFAULT_USER)
+ prog = os.path.join(self.ds_paths.sbin_dir, 'ns-slapd')
+
+ if self.status():
+ log.error("bak2db: Can not operate while directory server is
running")
+ return False
+
+ if not archive_dir:
+ log.error("bak2db: backup directory missing")
+ return False
+
+ result = subprocess.check_output([
+ prog,
+ 'archive2db',
+ '-a', archive_dir,
+ '-D', self.get_config_dir()
+ ])
+ u_result = ensure_str(result)
+
+ log.debug("bak2db output: BEGIN")
+ for line in u_result.split("\n"):
+ log.debug(line)
+ log.debug("bak2db output: END")
+
+ return True
+
+ def db2bak(self, archive_dir):
+ """
+ @param archive_dir - The directory to write the backup to
+ @return - True if the backup succeeded
+ """
+ DirSrvTools.lib389User(user=DEFAULT_USER)
+ prog = os.path.join(self.ds_paths.sbin_dir, 'ns-slapd')
+
+ if self.status():
+ log.error("db2bak: Can not operate while directory server is
running")
+ return False
+
+ if not archive_dir:
+ log.error("db2bak: archive directory missing")
+ return False
+
+ result = subprocess.check_output([
+ prog,
+ 'db2archive',
+ '-a', archive_dir,
+ '-D', self.get_config_dir()
+ ])
+ u_result = ensure_str(result)
+
+ log.debug("db2bak output: BEGIN")
+ for line in u_result.split("\n"):
+ log.debug(line)
+ log.debug("db2bak output: END")
+
+ return True
+
+ def db2index(self, bename=None, suffixes=None, attrs=None, vlvTag=None):
+ """
+ @param bename - The backend name to reindex
+ @param suffixes - List/tuple of suffixes to reindex
+ @param attrs - List/tuple of the attributes to index
+ @param vlvTag - The VLV index name to index
+ @return - True if reindexing succeeded
+ """
+ DirSrvTools.lib389User(user=DEFAULT_USER)
+ prog = os.path.join(self.ds_paths.sbin_dir, DB2INDEX)
+
+ if not bename and not suffixes:
+ log.error("db2index: missing required backend name or suffix")
+ return False
+
+ cmd = '%s -Z %s' % (prog, self.serverid)
+ if bename:
+ cmd = cmd + ' -n %s' % bename
+ if suffixes:
+ for suffix in suffixes:
+ cmd = cmd + ' -s %s' % suffix
+ if attrs:
+ for attr in attrs:
+ cmd = cmd + ' -t %s' % attr
+ if vlvTag:
+ cmd = cmd + ' -T %s' % vlvTag
+
+ self.stop(timeout=10)
+ log.info('Running script: %s' % cmd)
+ result = True
+ try:
+ os.system(cmd)
+ except:
+ log.error("db2index: error executing %s" % cmd)
+ result = False
+ self.start(timeout=10)
+
+ return result
+
+ def dbscan(self, bename=None, index=None, key=None, width=None, isRaw=False):
+ """Wrapper around dbscan tool that analyzes and extracts
information
+ from an import Directory Server database file
+
+ :param bename: The backend name to scan
+ :param index: Index name (e.g., cn or cn.db) to scan
+ :param key: Index key to dump
+ :param id: Entry id to dump
+ :param width: Entry truncate size (bytes)
+ :param isRaw: Dump as a raw data
+ :returns: A dumped string
+ """
+
+ DirSrvTools.lib389User(user=DEFAULT_USER)
+ prog = os.path.join(self.ds_paths.bin_dir, DBSCAN)
+
+ if not bename:
+ log.error("dbscan: missing required backend name")
+ return False
+
+ if not index:
+ log.error("dbscan: missing required index name")
+ return False
+ elif '.db' in index:
+ indexfile = os.path.join(self.dbdir, bename, index)
+ else:
+ indexfile = os.path.join(self.dbdir, bename, index + '.db')
+
+ cmd = [prog, '-f', indexfile]
+
+ if 'id2entry' in index:
+ if key and key.isdigit():
+ cmd.extend(['-K', key])
+ else:
+ if key:
+ cmd.extend(['-k', key])
+
+ if width:
+ cmd.extend(['-t', width])
+
+ if isRaw:
+ cmd.append('-R')
+
+ self.stop(timeout=10)
+
+ log.info('Running script: %s' % cmd)
+ output = subprocess.check_output(cmd)
+
+ self.start(timeout=10)
+
+ return output
+
+ def searchAccessLog(self, pattern):
+ """
+ Search all the access logs
+ """
+ return DirSrvTools.searchFile(self.accesslog + "*", pattern)
+
+ def searchAuditLog(self, pattern):
+ """
+ Search all the audit logs
+ """
+ time.sleep(1)
+ return DirSrvTools.searchFile(self.auditlog + "*", pattern)
+
+ def searchErrorsLog(self, pattern):
+ """
+ Search all the error logs
+ """
+ time.sleep(1)
+ return DirSrvTools.searchFile(self.errlog + "*", pattern)
+
+ def detectDisorderlyShutdown(self):
+ """
+ Search the current errors log for a disorderly shutdown message
+ """
+ time.sleep(1)
+ return DirSrvTools.searchFile(self.errlog, DISORDERLY_SHUTDOWN)
+
+ def deleteLog(self, logtype, restart=True):
+ """
+ Delete all the logs for this log type.
+ """
+ if restart:
+ self.stop()
+ for log in glob.glob(logtype + "*"):
+ if os.path.isfile(log):
+ os.remove(log)
+ if restart:
+ self.start()
+
+ def deleteAccessLogs(self, restart=True):
+ """
+ Delete all the access logs.
+ """
+ self.deleteLog(self.accesslog, restart)
+
+ def deleteAuditLogs(self, restart=True):
+ """
+ Delete all the audit logs.
+ """
+ self.deleteLog(self.auditlog, restart)
+
+ def deleteErrorLogs(self, restart=True):
+ """
+ Delete all the error logs.
+ """
+ self.deleteLog(self.errlog, restart)
+
+ def deleteAllLogs(self, restart=True):
+ """
+ Delete all the logs.
+ """
+ self.stop()
+ self.deleteAccessLogs(restart=False)
+ self.deleteErrorLogs(restart=False)
+ self.deleteAuditLogs(restart=False)
+ self.start()
+
+ def get_effective_rights(self, sourcedn, base=DEFAULT_SUFFIX,
+ scope=ldap.SCOPE_SUBTREE, *args, **kwargs):
+ """
+ Conduct a search on effective rights for some object (sourcedn)
+ against a filter.
+ For arguments to this function, please see LDAPObject.search_s.
+ For example:
+
+ @param sourcedn - DN of entry to check
+ @param base - Base DN of the suffix to check
+ @param scope - search scope
+ @param args -
+ @param kwargs -
+ @return - ldap result
+
+ LDAPObject.search_s(base, scope[, filterstr='(objectClass=*)'
+ [, attrlist=None[, attrsonly=0]]]) -> list|None
+
+ The sourcedn is the object that is having it's rights checked against
+ all objects matched by filterstr
+ If sourcedn is '', anonymous is checked.
+ If you set targetattrs to "*" you will see ALL possible attributes
for
+ all possible objectclasses on the object.
+ If you set targetattrs to "+" you will see operation attributes only.
+ If you set targetattrs to "*@objectclass" you will only see the
+ attributes from that class.
+ You will want to look at entryLevelRights and attributeLevelRights in
+ the result.
+ entryLevelRights:
+ * a - add
+ * d - delete
+ * n - rename
+ * v - view
+ attributeLevelRights
+ * r - read
+ * s - search
+ * w - write to the attribute (add / replace)
+ * o - obliterate (Delete the attribute)
+ * c - Compare the attributes directory side
+ * W - self write the attribute
+ * O - self obliterate
+ """
+ # Is there a better way to do this check?
+ if not (MAJOR >= 3 or (MAJOR == 2 and MINOR >= 7)):
+ raise Exception("UNSUPPORTED EXTENDED OPERATION ON THIS VERSION "
+
+ "OF PYTHON")
+ ldap_result = None
+ # This may not be thread safe. Is there a better way to do this?
+ try:
+ gerc = GetEffectiveRightsControl(True, authzId='dn:' +
+ sourcedn.encode('UTF-8'))
+ sctrl = [gerc]
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ # ldap_result = self.search_s(base, scope, *args, **kwargs)
+ res = self.search(base, scope, *args, **kwargs)
+ restype, ldap_result = self.result(res)
+ finally:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, [])
+ return ldap_result
+
+ # Is there a better name for this function?
+ def dereference(self, deref, base=DEFAULT_SUFFIX, scope=ldap.SCOPE_SUBTREE,
+ *args, **kwargs):
+ """
+ Perform a search which dereferences values from attributes such as
+ member or unique member.
+ For arguments to this function, please see LDAPObject.search_s. For
+ example:
+
+ @param deref - Dereference query
+ @param base - Base DN of the suffix to check
+ @param scope - search scope
+ @param args -
+ @param kwargs -
+ @return - ldap result
+
+ LDAPObject.search_s(base, scope[, filterstr='(objectClass=*)'
+ [, attrlist=None[, attrsonly=0]]]) -> list|None
+
+ A deref query is of the format:
+
+ "<attribute to derference>:<deref attr1>,<deref
attr2>..."
+
+ "uniqueMember:dn,objectClass"
+
+ This will return the dn's and objectClasses of the dereferenced members
+ of the group.
+ """
+ if not (MAJOR >= 3 or (MAJOR == 2 and MINOR >= 7)):
+ raise Exception("UNSUPPORTED EXTENDED OPERATION ON THIS VERSION "
+
+ " OF PYTHON")
+
+ # This may not be thread safe. Is there a better way to do this?
+ try:
+ drc = DereferenceControl(True, deref=deref.encode('UTF-8'))
+ sctrl = [drc]
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+
+ # ldap_result = self.search_s(base, scope, *args, **kwargs)
+ res = self.search(base, scope, *args, **kwargs)
+ resp_type, resp_data, resp_msgid, decoded_resp_ctrls, _, _ = \
+ self.result4(res, add_ctrls=1,
+ resp_ctrl_classes={CONTROL_DEREF:
+ DereferenceControl})
+ finally:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, [])
+ return resp_data, decoded_resp_ctrls
+
+ def buildLDIF(self, num, ldif_file, suffix='dc=example,dc=com',
pyinstall=False):
+ """Generate a simple ldif file using the dbgen.pl script, and set
the
+ ownership and permissions to match the user that the server runs as.
+
+ @param num - number of entries to create
+ @param ldif_file - ldif file name(including the path)
+ @suffix - DN of the parent entry in the ldif file
+ @return - nothing
+ @raise - OSError
+ """
+ if (not self.ds_paths.perl_enabled or pyinstall):
+ raise Exception("Perl tools disabled on this system. Try dbgen py
module.")
+ else:
+ try:
+ os.system('%s -s %s -n %d -o %s' %
(os.path.join(self.ds_paths.bin_dir, 'dbgen.pl'), suffix, num, ldif_file))
+ os.chmod(ldif_file, 0o644)
+ if os.getuid() == 0:
+ # root user - chown the ldif to the server user
+ uid = pwd.getpwnam(self.userid).pw_uid
+ gid = grp.getgrnam(self.userid).gr_gid
+ os.chown(ldif_file, uid, gid)
+ except OSError as e:
+ log.exception('Failed to create ldif file (%s): error %d - %s' %
+ (ldif_file, e.errno, e.strerror))
+ raise e
+
+ def getConsumerMaxCSN(self, replica_entry):
+ """
+ Attempt to get the consumer's maxcsn from its database
+ """
+ host = replica_entry.getValue(AGMT_HOST)
+ port = replica_entry.getValue(AGMT_PORT)
+ suffix = replica_entry.getValue(REPL_ROOT)
+ error_msg = "Unavailable"
+
+ # Open a connection to the consumer
+ consumer = DirSrv(verbose=self.verbose)
+ args_instance[SER_HOST] = host
+ args_instance[SER_PORT] = int(port)
+ args_instance[SER_ROOT_DN] = self.binddn
+ args_instance[SER_ROOT_PW] = self.bindpw
+ args_standalone = args_instance.copy()
+ consumer.allocate(args_standalone)
+ try:
+ consumer.open()
+ except ldap.LDAPError as e:
+ self.log.debug('Connection to consumer (%s:%s) failed, error: %s' %
+ (host, port, str(e)))
+ return error_msg
+
+ # Get the replica id from supplier to compare to the consumer's rid
+ try:
+ replica_entries = self.replica.list(suffix)
+ if not replica_entries:
+ # Error
+ consumer.close()
+ return None
+ rid = replica_entries[0].getValue(REPL_ID)
+ except:
+ # Error
+ consumer.close()
+ return None
+
+ # Search for the tombstone RUV entry
+ try:
+ entry = consumer.search_s(suffix, ldap.SCOPE_SUBTREE,
+ REPLICA_RUV_FILTER, ['nsds50ruv'])
+ consumer.close()
+ if not entry:
+ # Error out?
+ return error_msg
+ elements = entry[0].getValues('nsds50ruv')
+ for ruv in elements:
+ if ('replica %s ' % rid) in ruv:
+ ruv_parts = ruv.split()
+ if len(ruv_parts) == 5:
+ return ruv_parts[4]
+ else:
+ return error_msg
+ return error_msg
+ except:
+ # Search failed, but return 0
+ consumer.close()
+ return error_msg
+
+ def getReplAgmtStatus(self, agmt_entry):
+ '''
+ Return the status message, if consumer is not in synch raise an
+ exception
+ '''
+ agmt_maxcsn = None
+ suffix = agmt_entry.getValue(REPL_ROOT)
+ agmt_name = agmt_entry.getValue('cn')
+ status = "Unknown"
+ rc = -1
+ try:
+ entry = self.search_s(suffix, ldap.SCOPE_SUBTREE,
+ REPLICA_RUV_FILTER, [AGMT_MAXCSN])
+ except:
+ return status
+
+ '''
+ There could be many agmts maxcsn attributes, find ours
+
+ agmtMaxcsn: <suffix>;<agmt
name>;<host>;<port>;<consumer rid>;<maxcsn>
+
+ dc=example,dc=com;test_agmt;localhost;389:4;56536858000100010000
+
+ or if the consumer is not reachable:
+
+ dc=example,dc=com;test_agmt;localhost;389;unavailable
+
+ '''
+ maxcsns = entry[0].getValues(AGMT_MAXCSN)
+ for csn in maxcsns:
+ comps = csn.split(';')
+ if agmt_name == comps[1]:
+ # same replica, get maxcsn
+ if len(comps) < 6:
+ return "Consumer unavailable"
+ else:
+ agmt_maxcsn = comps[5]
+
+ if agmt_maxcsn:
+ con_maxcsn = self.getConsumerMaxCSN(agmt_entry)
+ if con_maxcsn:
+ if agmt_maxcsn == con_maxcsn:
+ status = "In Synchronization"
+ rc = 0
+ else:
+ # Not in sync - attmpt to discover the cause
+ repl_msg = "Unknown"
+ if agmt_entry.getValue(AGMT_UPDATE_IN_PROGRESS) == 'TRUE':
+ # Replication is on going - this is normal
+ repl_msg = "Replication still in progress"
+ elif "Can't Contact LDAP" in \
+ agmt_entry.getValue(AGMT_UPDATE_STATUS):
+ # Consumer is down
+ repl_msg = "Consumer can not be contacted"
+
+ status = ("Not in Synchronization: supplier " +
+ "(%s) consumer (%s) Reason(%s)" %
+ (agmt_maxcsn, con_maxcsn, repl_msg))
+ if rc != 0:
+ raise ValueError(status)
+ return status
+
+ # This could be made to delete by filter ....
+ def delete_branch_s(self, basedn, scope):
+ ents = self.search_s(basedn, scope)
+ for ent in ents:
+ self.log.debug("Delete entry children %s" % (ent.dn))
+ self.delete_s(ent.dn)
diff --git a/src/lib389/lib389/_constants.py b/src/lib389/lib389/_constants.py
new file mode 100644
index 0000000..1d458dd
--- /dev/null
+++ b/src/lib389/lib389/_constants.py
@@ -0,0 +1,309 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import os
+from enum import Enum
+from lib389.properties import *
+
+(
+ MASTER_TYPE,
+ HUB_TYPE,
+ LEAF_TYPE
+) = list(range(3))
+
+INSTALL_LATEST_CONFIG = '999999999'
+
+REPLICA_FLAGS_CON = 0
+
+# The structure is convenient for replica promote/demote methods
+ReplicaRole = Enum("Replica role", "CONSUMER HUB MASTER STANDALONE")
+
+CONSUMER_REPLICAID = 65535
+
+REPLICA_RDONLY_TYPE = 2 # CONSUMER and HUB
+REPLICA_WRONLY_TYPE = 1 # SINGLE and MULTI MASTER
+REPLICA_RDWR_TYPE = REPLICA_RDONLY_TYPE | REPLICA_WRONLY_TYPE
+
+REPLICA_FLAGS_RDONLY = 0
+REPLICA_FLAGS_WRITE = 1
+
+REPLICA_RUV_UUID = "ffffffff-ffffffff-ffffffff-ffffffff"
+REPLICA_RUV_FILTER =
('(&(nsuniqueid=ffffffff-ffffffff-ffffffff-ffffffff)(objectclass=nstombstone))')
+REPLICA_OC_TOMBSTONE = "nsTombstone"
+REPLICATION_BIND_DN = RA_BINDDN
+REPLICATION_BIND_PW = RA_BINDPW
+REPLICATION_BIND_METHOD = RA_METHOD
+REPLICATION_TRANSPORT = RA_TRANSPORT_PROT
+REPLICATION_TIMEOUT = RA_TIMEOUT
+
+TRANS_STARTTLS = "starttls"
+TRANS_SECURE = "secure"
+TRANS_NORMAL = "normal"
+REPL_TRANS_VALUE = {
+ TRANS_STARTTLS: 'TLS',
+ TRANS_SECURE: 'SSL',
+ TRANS_NORMAL: 'LDAP'
+ }
+
+defaultProperties = {
+ REPLICATION_BIND_DN: "cn=replrepl,cn=config",
+ REPLICATION_BIND_PW: "password",
+ REPLICATION_BIND_METHOD: "simple",
+ REPLICATION_TRANSPORT: REPL_TRANS_VALUE[TRANS_NORMAL],
+ REPLICATION_TIMEOUT: str(120)
+ }
+
+
+CFGSUFFIX = "o=NetscapeRoot"
+
+# Some DN constants
+DN_DM = "cn=Directory Manager"
+PW_DM = "password"
+DN_CONFIG = "cn=config"
+DN_LDBM = "cn=ldbm database,cn=plugins,cn=config"
+DN_CONFIG_LDBM = "cn=config,cn=ldbm database,cn=plugins,cn=config"
+DN_USERROOT_LDBM = "cn=userRoot,cn=ldbm database,cn=plugins,cn=config"
+DN_SCHEMA = "cn=schema"
+DN_MONITOR = "cn=monitor"
+DN_MONITOR_SNMP = "cn=snmp,cn=monitor"
+DN_MONITOR_LDBM = "cn=monitor,cn=ldbm database,cn=plugins,cn=config"
+
+
+CMD_PATH_SETUP_DS = "setup-ds.pl"
+CMD_PATH_REMOVE_DS = "remove-ds.pl"
+CMD_PATH_SETUP_DS_ADMIN = "setup-ds-admin.pl"
+CMD_PATH_REMOVE_DS_ADMIN = "remove-ds-admin.pl"
+
+# State of an DirSrv object
+DIRSRV_STATE_INIT = 1
+DIRSRV_STATE_ALLOCATED = 2
+DIRSRV_STATE_OFFLINE = 3
+DIRSRV_STATE_RUNNING = 4
+DIRSRV_STATE_ONLINE = 5
+
+# So uh .... localhost.localdomain doesn't always exist. Stop. Using. It.
+# LOCALHOST = "localhost.localdomain"
+LOCALHOST = "localhost"
+LOCALHOST_SHORT = "localhost"
+DEFAULT_PORT = 389
+DEFAULT_SECURE_PORT = 636
+DEFAULT_SUFFIX = 'dc=example,dc=com'
+DEFAULT_SUFFIX_ESCAPED = 'dc\3Dexample\2Cdc\3Dcom'
+DEFAULT_DOMAIN = 'example.com'
+DEFAULT_BENAME = 'userRoot' # warning it is case sensitive
+DEFAULT_BACKUPDIR = '/tmp'
+DEFAULT_INST_HEAD = 'slapd-'
+DEFAULT_ENV_HEAD = 'dirsrv-'
+DEFAULT_CHANGELOG_NAME = "changelog5"
+DEFAULT_CHANGELOG_DB = 'changelogdb'
+
+# CONF_DIR = 'etc/dirsrv'
+# ENV_SYSCONFIG_DIR = '/etc/sysconfig'
+ENV_LOCAL_DIR = '.dirsrv'
+
+# CONFIG file (<prefix>/etc/sysconfig/dirsrv-* or
+# $HOME/.dirsrv/dirsrv-*) keywords
+CONF_SERVER_ID = 'SERVER_ID'
+CONF_SERVER_DIR = 'SERVER_DIR'
+CONF_SERVERBIN_DIR = 'SERVERBIN_DIR'
+CONF_CONFIG_DIR = 'CONFIG_DIR'
+CONF_INST_DIR = 'INST_DIR'
+CONF_RUN_DIR = 'RUN_DIR'
+CONF_DS_ROOT = 'DS_ROOT'
+CONF_PRODUCT_NAME = 'PRODUCT_NAME'
+CONF_LDAPI_ENABLED = 'LDAPI_ENABLED'
+CONF_LDAPI_SOCKET = 'LDAPI_SOCKET'
+CONF_LDAPI_AUTOBIND = 'LDAPI_AUTOBIND'
+CONF_LDAPI_ROOTUSER = 'LDAPI_ROOTUSER'
+
+DN_CONFIG = "cn=config"
+DN_PLUGIN = "cn=plugins,%s" % DN_CONFIG
+DN_MAPPING_TREE = "cn=mapping tree,%s" % DN_CONFIG
+DN_CHANGELOG = "cn=changelog5,%s" % DN_CONFIG
+DN_LDBM = "cn=ldbm database,%s" % DN_PLUGIN
+DN_CHAIN = "cn=chaining database,%s" % DN_PLUGIN
+
+DN_TASKS = "cn=tasks,%s" % DN_CONFIG
+DN_INDEX_TASK = "cn=index,%s" % DN_TASKS
+DN_EXPORT_TASK = "cn=export,%s" % DN_TASKS
+DN_IMPORT_TASK = "cn=import,%s" % DN_TASKS
+DN_BACKUP_TASK = "cn=backup,%s" % DN_TASKS
+DN_RESTORE_TASK = "cn=restore,%s" % DN_TASKS
+DN_MBO_TASK = "cn=memberOf task,%s" % DN_TASKS
+DN_TOMB_FIXUP_TASK = "cn=fixup tombstones,%s" % DN_TASKS
+
+# Script Constants
+LDIF2DB = 'ldif2db'
+DB2LDIF = 'db2ldif'
+BAK2DB = 'bak2db'
+DB2BAK = 'db2bak'
+DB2INDEX = 'db2index'
+DBSCAN = 'dbscan'
+
+RDN_REPLICA = "cn=replica"
+
+RETROCL_SUFFIX = "cn=changelog"
+
+##################################
+#
+# Request Control OIDS
+#
+##################################
+CONTROL_DEREF = '1.3.6.1.4.1.4203.666.5.16'
+
+##################################
+#
+# Plugins
+#
+##################################
+
+PLUGIN_7_BIT_CHECK = '7-bit check'
+PLUGIN_ACCT_POLICY = 'Account Policy Plugin'
+PLUGIN_ACCT_USABILITY = 'Account Usability Plugin'
+PLUGIN_ACL = 'ACL Plugin'
+PLUGIN_ACL_PREOP = 'ACL preoperation'
+PLUGIN_ATTR_UNIQUENESS = 'attribute uniqueness'
+PLUGIN_AUTOMEMBER = 'Auto Membership Plugin'
+PLUGIN_CHAININGDB = 'chaining database'
+PLUGIN_COLLATION = 'Internationalization Plugin'
+PLUGIN_COS = 'Class of Service'
+PLUGIN_DEREF = 'deref'
+PLUGIN_DNA = 'Distributed Numeric Assignment Plugin'
+PLUGIN_HTTP = 'HTTP Client'
+PLUGIN_LINKED_ATTRS = 'Linked Attributes'
+PLUGIN_MANAGED_ENTRY = 'Managed Entries'
+PLUGIN_MEMBER_OF = 'MemberOf Plugin'
+PLUGIN_PAM_PASSTHRU = 'PAM Pass Through Auth'
+PLUGIN_PASSTHRU = 'Pass Through Authentication'
+PLUGIN_POSIX_WINSYNC = 'Posix Winsync API'
+PLUGIN_REFER_INTEGRITY = 'referential integrity postoperation'
+PLUGIN_REPL_SYNC = 'Content Synchronization'
+PLUGIN_REPLICATION_LEGACY = 'Legacy Replication Plugin'
+PLUGIN_REPLICATION = 'Multimaster Replication Plugin'
+PLUGIN_RETRO_CHANGELOG = 'Retro Changelog Plugin'
+PLUGIN_ROLES = 'Roles Plugin'
+PLUGIN_ROOTDN_ACCESS = 'RootDN Access Control'
+PLUGIN_SCHEMA_RELOAD = 'Schema Reload'
+PLUGIN_STATECHANGE = 'State Change Plugin'
+PLUGIN_USN = 'USN'
+PLUGIN_VIEWS = 'Views'
+PLUGIN_WHOAMI = 'whoami'
+PLUGIN_ADDN = 'addn'
+
+
+#
+# Constants
+#
+DEFAULT_USER = "dirsrv"
+DEFAULT_USERHOME = "/tmp/lib389_home"
+DEFAULT_USER_COMMENT = "lib389 DS user"
+DATA_DIR = "data"
+TMP_DIR = "tmp"
+VALGRIND_WRAPPER = "ns-slapd.valgrind"
+VALGRIND_LEAK_STR = " blocks are definitely lost in loss record "
+VALGRIND_INVALID_STR = " Invalid (free|read|write)"
+DISORDERLY_SHUTDOWN = ('Detected Disorderly Shutdown last time Directory '
+ 'Server was running, recovering database')
+
+#
+# LOG: see
https://access.redhat.com/documentation/en-US/Red_Hat_Directory
+# _Server/10/html/Administration_Guide/Configuring_Logs.html
+# The default log level is 16384
+#
+(LOG_TRACE,
+ LOG_TRACE_PACKETS,
+ LOG_TRACE_HEAVY,
+ LOG_CONNECT,
+ LOG_PACKET,
+ LOG_SEARCH_FILTER,
+ LOG_CONFIG_PARSER,
+ LOG_ACL,
+ LOG_ENTRY_PARSER,
+ LOG_HOUSEKEEPING,
+ LOG_REPLICA,
+ LOG_DEFAULT,
+ LOG_CACHE,
+ LOG_PLUGIN,
+ LOG_MICROSECONDS,
+ LOG_ACL_SUMMARY) = [1 << x for x in (list(range(8)) + list(range(11, 19)))]
+
+
+#
+# Constants for individual tests
+#
+SUFFIX = 'dc=example,dc=com'
+PASSWORD = 'password'
+
+# Generate instance parameters on the fly
+N=0
+port_start = 38901
+number_of_instances = 99
+
+# Standalone topology
+for i in range(port_start, port_start + number_of_instances):
+ N+=1
+ exec("HOST_STANDALONE{0} = {1}".format(N, "LOCALHOST"))
+ exec("PORT_STANDALONE{0} = {1}".format(N, i))
+ exec("SECUREPORT_STANDALONE{0} = {1}".format(N, i + 24700))
+ exec("SERVERID_STANDALONE{0} = {1}".format(N,
"\"standalone{0}\"".format(N)))
+ exec("REPLICAID_STANDALONE_{0} = {1}".format(N, 65535))
+
+# For compatibility
+HOST_STANDALONE = HOST_STANDALONE1
+PORT_STANDALONE = PORT_STANDALONE1
+SECUREPORT_STANDALONE = SECUREPORT_STANDALONE1
+SERVERID_STANDALONE = SERVERID_STANDALONE1
+
+# Replication topology - masters
+N=0
+port_start+=100
+for i in range(port_start, port_start + number_of_instances):
+ N+=1
+ exec("HOST_MASTER_{0} = {1}".format(N, "LOCALHOST"))
+ exec("PORT_MASTER_{0} = {1}".format(N, i))
+ exec("SECUREPORT_MASTER_{0} = {1}".format(N, i + 24700))
+ exec("SERVERID_MASTER_{0} = {1}".format(N,
"\"master{0}\"".format(N)))
+ exec("REPLICAID_MASTER_{0} = {1}".format(N, N))
+
+# Replication topology - hubs
+N=0
+port_start+=100
+for i in range(port_start, port_start + number_of_instances):
+ N+=1
+ exec("HOST_HUB_{0} = {1}".format(N, "LOCALHOST"))
+ exec("PORT_HUB_{0} = {1}".format(N, i))
+ exec("SECUREPORT_HUB_{0} = {1}".format(N, i + 24700))
+ exec("SERVERID_HUB_{0} = {1}".format(N,
"\"hub{0}\"".format(N)))
+ exec("REPLICAID_HUB_{0} = {1}".format(N, 65535))
+
+# Replication topology - consumers
+N=0
+port_start+=100
+for i in range(port_start, port_start + number_of_instances):
+ N+=1
+ exec("HOST_CONSUMER_{0} = {1}".format(N, "LOCALHOST"))
+ exec("PORT_CONSUMER_{0} = {1}".format(N, i))
+ exec("SECUREPORT_CONSUMER_{0} = {1}".format(N, i + 24700))
+ exec("SERVERID_CONSUMER_{0} = {1}".format(N,
"\"consumer{0}\"".format(N)))
+
+# Cleanup, we don't need to export that
+del N, port_start, number_of_instances
+
+# This is a template
+args_instance = {SER_DEPLOYED_DIR: os.environ.get('PREFIX', None),
+ SER_BACKUP_INST_DIR: os.environ.get('BACKUPDIR',
+ DEFAULT_BACKUPDIR),
+ SER_ROOT_DN: DN_DM,
+ SER_ROOT_PW: PW_DM,
+ SER_HOST: LOCALHOST,
+ SER_PORT: DEFAULT_PORT,
+ SER_SERVERID_PROP: "template",
+ SER_CREATION_SUFFIX: DEFAULT_SUFFIX}
+
+# Helper for linking dse.ldif values to the parse_config function
+args_dse_keys = SER_PROPNAME_TO_ATTRNAME
diff --git a/src/lib389/lib389/_controls.py b/src/lib389/lib389/_controls.py
new file mode 100644
index 0000000..330b64b
--- /dev/null
+++ b/src/lib389/lib389/_controls.py
@@ -0,0 +1,137 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""
+Lib389 python ldap request controls.
+
+These should be upstreamed into python ldap when possible.
+"""
+
+from ldap.controls import LDAPControl
+from pyasn1.type import namedtype, univ, tag
+from pyasn1.codec.ber import encoder, decoder
+from pyasn1_modules.rfc2251 import AttributeDescription, LDAPDN, AttributeValue
+from lib389._constants import *
+
+# Could use AttributeDescriptionList
+
+"""
+ controlValue ::= SEQUENCE OF derefSpec DerefSpec
+
+ DerefSpec ::= SEQUENCE {
+ derefAttr attributeDescription, ; with DN syntax
+ attributes AttributeList }
+
+ AttributeList ::= SEQUENCE OF attr AttributeDescription
+
+ Needs to be matched by ber_scanf(ber, "{a{v}}", ... )
+"""
+
+
+class AttributeList(univ.SequenceOf):
+ componentType = AttributeDescription()
+
+
+class DerefSpec(univ.Sequence):
+ componentType = namedtype.NamedTypes(
+ namedtype.NamedType('derefAttr', AttributeDescription()),
+ namedtype.NamedType('attributes', AttributeList()),
+ )
+
+
+class DerefControlValue(univ.SequenceOf):
+ componentType = DerefSpec()
+
+"""
+ controlValue ::= SEQUENCE OF derefRes DerefRes
+
+ DerefRes ::= SEQUENCE {
+ derefAttr AttributeDescription,
+ derefVal LDAPDN,
+ attrVals [0] PartialAttributeList OPTIONAL }
+
+ PartialAttributeList ::= SEQUENCE OF
+ partialAttribute PartialAttribute
+"""
+
+
+class Vals(univ.SetOf):
+ componentType = AttributeValue()
+
+
+class PartialAttribute(univ.Sequence):
+ componentType = namedtype.NamedTypes(
+ namedtype.NamedType('type', AttributeDescription()),
+ namedtype.NamedType('vals', Vals()),
+ )
+
+
+class PartialAttributeList(univ.SequenceOf):
+ componentType = PartialAttribute()
+ tagSet = univ.Sequence.tagSet.tagImplicitly(tag.Tag(tag.tagClassContext,
+ tag.tagFormatConstructed, 0))
+
+
+class DerefRes(univ.Sequence):
+ componentType = namedtype.NamedTypes(
+ namedtype.NamedType('derefAttr', AttributeDescription()),
+ namedtype.NamedType('derefVal', LDAPDN()),
+ namedtype.OptionalNamedType('attrVals', PartialAttributeList()),
+ )
+
+
+class DerefResultControlValue(univ.SequenceOf):
+ componentType = DerefRes()
+
+
+class DereferenceControl(LDAPControl):
+ """
+ Dereference Control
+ """
+
+ def __init__(self, criticality=True, deref=None):
+ LDAPControl.__init__(self, CONTROL_DEREF, criticality)
+ self.deref = deref
+
+ def encodeControlValue(self):
+ cv = DerefControlValue()
+ cvi = 0
+ for derefSpec in self.deref.split(';'):
+ derefAttr, attributes = derefSpec.split(':')
+ attributes = attributes.split(',')
+ al = AttributeList()
+ i = 0
+ while len(attributes) > 0:
+ al.setComponentByPosition(i, attributes.pop())
+ i += 1
+ ds = DerefSpec()
+ ds.setComponentByName('derefAttr', derefAttr)
+ ds.setComponentByName('attributes', al)
+ cv.setComponentByPosition(cvi, ds)
+ cvi += 1
+ return encoder.encode(cv)
+
+ def decodeControlValue(self, encodedControlValue):
+ self.entry = []
+ # debug.setLogger(debug.flagAll)
+ decodedValue, _ = decoder.decode(encodedControlValue,
+ asn1Spec=DerefResultControlValue())
+
+ for derefres in decodedValue:
+ result = {}
+ result['derefAttr'] =
str(derefres.getComponentByName('derefAttr'))
+ result['derefVal'] =
str(derefres.getComponentByName('derefVal'))
+ result['attrVals'] = []
+ for attrval in derefres.getComponentByName('attrVals'):
+ av = {}
+ av['type'] = str(attrval.getComponentByName('type'))
+ av['vals'] = []
+ for val in attrval.getComponentByName('vals'):
+ av['vals'].append(str(val))
+ result['attrVals'].append(av)
+ self.entry.append(result)
diff --git a/src/lib389/lib389/_entry.py b/src/lib389/lib389/_entry.py
new file mode 100644
index 0000000..d13f458
--- /dev/null
+++ b/src/lib389/lib389/_entry.py
@@ -0,0 +1,631 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import re
+import six
+import logging
+import ldif
+import ldap
+import binascii
+from ldap.cidict import cidict
+import sys
+
+from lib389._constants import *
+from lib389.properties import *
+from lib389.utils import ensure_str, ensure_bytes, ensure_list_bytes
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+logging.basicConfig(level=logging.DEBUG)
+log = logging.getLogger(__name__)
+
+
+class FormatDict(cidict):
+ def __getitem__(self, name):
+ if name in self:
+ return ' '.join(cidict.__getitem__(self, name))
+ return None
+
+
+class Entry(object):
+ """This class represents an LDAP Entry object.
+
+ An LDAP entry consists of a DN and a list of attributes.
+ Each attribute consists of a name and a *list* of values.
+ String values will be rendered badly!
+ ex. {
+ 'uid': ['user01'],
+ 'cn': ['User'],
+ 'objectlass': [ 'person', 'inetorgperson' ]
+ }
+
+ In python-ldap, entries are returned as a list of 2-tuples.
+ Instance variables:
+ dn - string - the string DN of the entry
+ data - cidict - case insensitive dict of the attributes and values
+ """
+ # the ldif class base64 encodes some attrs which I would rather see in raw
+ # form - to encode specific attrs as base64, add them to the list below
+ base64_attrs = ['nsstate']
+
+ def __init__(self, entrydata):
+ """entrydata is the raw data returned from the python-ldap
+ result method, which is:
+ * a search result entry -> (dn, {dict...} )
+ * or a reference -> (None, reference)
+ * or None.
+
+ If creating a new empty entry, data is the string DN.
+ """
+ self.ref = None
+ if entrydata:
+ if isinstance(entrydata, tuple):
+ if entrydata[0] is None:
+ self.ref = entrydata[1] # continuation reference
+ else:
+ self.dn = entrydata[0]
+ self.data = cidict(entrydata[1])
+ elif isinstance(entrydata, six.string_types):
+ if '=' not in entrydata:
+ raise ValueError('Entry dn must contain "="')
+ self.dn = entrydata
+ self.data = cidict()
+ else:
+ self.data = cidict()
+ self.dn = None
+
+ def __bool__(self):
+ """
+ This allows us to do tests like if entry: returns false if there
+ is no data, true otherwise
+ """
+ return self.data is not None and len(self.data) > 0
+
+ def __eq__(self, other):
+ """
+ Compare if two Entry objects are the same.
+
+ This is good for fast comparison of the data we have,
+ but it relies on a few things.
+
+ Each Entry must have been searched and retrieved with the
+ same filter and attribute requirements.
+ If that isn't the case, this will immediately fail.
+ """
+ # We could make this "strict" by forcing this to pull back all
+ # the attributes of the DN, and the nsUniqueID.
+ # Guard from accidents
+ if not isinstance(other, Entry):
+ return False
+ # Check that our DN is the same.
+ if self.dn != other.dn:
+ return False
+ # Bail fast if the keys don't match
+ if set(self.getAttrs()) != set(other.getAttrs()):
+ return False
+ # Check the values of each key
+ for key in self.getAttrs():
+ if set(self.getValues(key)) != set(other.getValues(key)):
+ return False
+ return True
+
+ def __ne__(self, other):
+ """
+ Compare if two Entry objects are the same.
+
+ See __eq__ for a description of limitations.
+ """
+ return not self.__eq__(other)
+
+ def hasAttr(self, name):
+ """
+ Return True if this entry has an attribute named name, False otherwise
+ """
+ if self.data is None:
+ # Perhaps this should be an exception?
+ raise Exception('Invalid data state in Entry')
+ # We can't actually enforce this because cidict doesn't inherit Mapping
+ # if not isinstance(self.data, collections.Mapping):
+ # raise Exception('Invalid data type for Entry')
+ return ensure_str(name) in self.data
+
+ def __getitem__(self, name):
+ # This should probably return getValues?
+ return self.__getattr__(name)
+
+ def __getattr__(self, name):
+ """
+ If name is the name of an LDAP attribute, return the first
+ value for that attribute - equivalent to getValue - this allows
+ the use of
+ entry.cn
+ instead of
+ entry.getValue('cn')
+ This also allows us to return None if an attribute is not found
+ rather than throwing an exception
+ """
+ if name == 'dn' or name == 'data':
+ return self.__dict__.get(name, None)
+ return self.getValue(name)
+
+ def getValuesSet(self, keys):
+ """
+ returns a set of values based on keys
+ """
+ result = {}
+ for k in keys:
+ result[k] = self.data.get(k, [])
+ return result
+
+ def getValues(self, name):
+ """Get the list (array) of values for the attribute named
name"""
+ return self.data.get(name, [])
+
+ def getValue(self, name):
+ """Get the first value for the attribute named
name"""
+ return self.data.get(name, [None])[0]
+
+ def hasValue(self, name, val=None):
+ """True if the given attribute is present and has the given value
+
+ TODO: list comparison preserves order: should I use a set?
+ """
+ if not self.hasAttr(name):
+ return False
+ if not val:
+ return True
+ if isinstance(val, list):
+ return val == self.data.get(name)
+ if isinstance(val, tuple):
+ return list(val) == self.data.get(name)
+ return ensure_bytes(val) in self.data.get(name)
+
+ def hasValueCase(self, name, val):
+ """
+ True if the given attribute is present and has the given value -
+ ase insensitive value match
+ """
+ if not self.hasAttr(name):
+ return False
+ return val.lower() in [x.lower() for x in self.data.get(name)]
+
+ def setValue(self, name, *value):
+ """
+ Value passed in may be a single value, several values,
+ or a single sequence.
+ For example:
+ ent.setValue('name', 'value')
+ ent.setValue('name', 'value1', 'value2', ...,
'valueN')
+ ent.setValue('name', ['value1', 'value2', ...,
'valueN'])
+ ent.setValue('name', ('value1', 'value2', ...,
'valueN'))
+ Since *value is a tuple, we may have to extract a list or tuple
+ from that tuple as in the last two examples above
+ """
+ if isinstance(value[0], list) or isinstance(value[0], tuple):
+ self.data[name] = value[0]
+ else:
+ self.data[name] = value
+
+ def getAttrs(self):
+ if not self.data:
+ return []
+ return list(self.data.keys())
+
+ def iterAttrs(self, attrsOnly=False):
+ if attrsOnly:
+ return six.iterkeys(self.data)
+ else:
+ return six.iteritems(self.data)
+
+ setValues = setValue
+
+ def toTupleList(self):
+ """
+ Convert the attrs and values to a list of 2-tuples. The first
+ element of the tuple is the attribute name. The second element
+ is either a single value or a list of values.
+ """
+ # For python3, we have to make sure EVERYTHING is a byte string.
+ # Else everything EXPLODES
+ lt = None
+ if MAJOR >= 3:
+ # This converts the dict to a list of tuples,
+ lt = []
+ for k in self.data.keys():
+ # l here is the
+ vals = None
+ if isinstance(self.data[k], list) or isinstance(self.data[k], tuple):
+ vals = ensure_list_bytes(self.data[k])
+ else:
+ vals = ensure_list_bytes([self.data[k]])
+ lt.append((k, vals))
+ # lt is now complete.
+ else:
+ lt = list(self.data.items())
+ return lt
+
+ def getref(self):
+ return self.ref
+
+ def __str__(self):
+ """
+ Convert the Entry to its LDIF representation
+ """
+ return self.__repr__()
+
+ def update(self, dct):
+ """Update passthru to the data attribute."""
+ log.debug("update dn: %r with %r" % (self.dn, dct))
+ for k, v in list(dct.items()):
+ if isinstance(v, list) or isinstance(v, tuple):
+ self.data[k] = v
+ else:
+ self.data[k] = [v]
+
+ def __repr__(self):
+ """Convert the Entry to its LDIF representation"""
+ sio = six.StringIO()
+ """
+ what's all this then? the unparse method will currently only accept
+ a list or a dict, not a class derived from them. self.data is a
+ cidict, so unparse barfs on it. I've filed a bug against
+ python-ldap, but in the meantime, we have to convert to a plain old
+ dict for printing.
+ I also don't want to see wrapping, so set the line width really high
+ (1000)
+ """
+ newdata = {}
+ for k, v in self.data.items():
+ newdata[k] = ensure_list_bytes(v)
+
+ ldif.LDIFWriter(
+ sio, Entry.base64_attrs, 1000).unparse(self.dn, newdata)
+ return sio.getvalue()
+
+ def bin2b64(self):
+ """
+ Convert any binary values in the entry to base 64
+ """
+ for attr, vals in self.iterAttrs():
+ attr_vals = []
+ for val in vals:
+ try:
+ val.decode('ascii')
+ attr_vals.append(val)
+ except:
+ # We have a binary value we need to convert
+ b64 = binascii.b2a_base64(val)
+ if b64[-1] == '\n':
+ # Strip off the newline
+ b64 = b64[:-1]
+
+ attr_vals.append(b64)
+
+ self.data[attr] = attr_vals
+
+ def getJSONEntry(self):
+ """
+ Return a JSON dictionary representation of the entry:
+
+ dn - Entry DN
+ attrs - entry's attributes
+
+ {
+ "dn": "uid=user,dc=example,dc=com",
+ "attrs": {
+ "objectClass": [
+ "top",
+ "person",
+ "organizationalPerson",
+ "inetorgperson",
+ "inetuser"
+ ],
+ "givenName": [
+ "user"
+ ],
+ "uid": [
+ "user"
+ ],
+ "sn": [
+ "user lastname"
+ ],
+ "cn": [
+ "user name"
+ ]
+ }
+ }
+ """
+ # Need to convert binary values to base64 for our JSON representations
+ self.bin2b64()
+
+ entry = {'dn': self.dn, 'attrs': dict(self.data)}
+ return entry
+
+ def create(self, type=ENTRY_TYPE_PERSON, entry_dn=None, properties=None):
+ """
+ Return - eventually creating a person entry with the given dn and
+ pwd. binddn can be a lib389.Entry
+ """
+ if not entry_dn:
+ raise ValueError("entry_dn is mandatory")
+
+ if not type:
+ raise ValueError("type is mandatory")
+
+ ent = Entry(entry_dn)
+ ent.setValues(ENTRY_OBJECTCLASS, ENTRY_TYPE_TO_OBJECTCLASS[type])
+ ent.setValues(ENTRY_USERPASSWORD, properties.get(ENTRY_USERPASSWORD,
+ ""))
+ ent.setValues(ENTRY_SN, properties.get(ENTRY_SN,
+ "bind dn pseudo user"))
+ ent.setValues(ENTRY_CN, properties.get(ENTRY_CN,
+ "bind dn pseudo user"))
+ if type == ENTRY_TYPE_INETPERSON:
+ ent.setValues(ENTRY_UID, properties.get(ENTRY_UID,
+ "bind dn pseudo user"))
+
+ try:
+ self.add_s(ent)
+ except ldap.ALREADY_EXISTS:
+ log.warn("Entry %s already exists" % entry_dn)
+
+ try:
+ entry = self._test_entry(entry_dn, ldap.SCOPE_BASE)
+ return entry
+ except MissingEntryError:
+ log.exception("This entry should exist!")
+ raise
+
+ def getAcis(self):
+ if not self.hasAttr('aci'):
+ # There should be a better way to do this? Perhaps
+ # self search for the aci attr?
+ return []
+ self.acis = [EntryAci(self, a, verbose=False) for a in
self.getValues('aci')]
+
+ return self.acis
+
+
+class EntryAci(object):
+ """Breaks down an aci attribute string from 389, into a dictionary
+ of terms and values. These values can then be manipulated, and
+ subsequently rebuilt into an aci string.
+
+ :param entry: An entry
+ :type entry: lib389._entry.Entry
+ :param rawaci: Aci in a raw form
+ :type rawaci: str
+ :param verbose: False by default
+ :type verbose: bool
+ """
+
+ # We seperate the keys into 3 groups, and one group that has overlap.
+ # This is so we can not only split the aci, but rebuild it from the
+ # dictionary at a later point in time.
+
+ # These are top level aci comoponent keys
+ _keys = ['targetscope',
+ 'targetattrfilters',
+ 'targattrfilters',
+ 'targetfilter',
+ 'targetattr',
+ 'target',
+ 'version 3.0;']
+ # These are the keys which are seperated by ; in the version 3.0 stanza.
+ _v3keys = ['allow',
+ 'acl',
+ 'deny']
+ # These are the keys which are used on the inside of a v3 allow statement
+ # We have them defined, but don't currently use them.
+ _v3innerkeys = ['roledn',
+ 'userattr',
+ 'ip',
+ 'dns',
+ 'dayofweek',
+ 'timeofday',
+ 'authmethod',
+ 'userdn',
+ 'groupdn']
+ # These keys values are prefixed with ldap:///, so we need to know to
+ # re-prefix ldap:/// onto the value when we rebuild the aci
+ _urlkeys = ['target',
+ 'userdn',
+ 'groupdn',
+ 'roledn']
+
+ def __init__(self, entry, rawaci, verbose=False):
+ """
+ """
+ self.verbose = verbose
+ self.entry = entry
+ self._rawaci = ensure_str(rawaci)
+ self.acidata = self._parse_aci(self._rawaci)
+
+ def __eq__(self, other):
+
+ # If we are from different entries, we should fail
+ # Can this fail if we have different filters to get the acis though?
+ if self.entry.dn != other.entry.dn:
+ return False
+ # Should we be checking the generated Aci incase it has changed?
+ return self.getRawAci() == other.getRawAci()
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def _format_term(self, key, value_dict):
+ rawaci = ''
+ if value_dict.get('equal', True):
+ rawaci += '="'
+ else:
+ rawaci += '!="'
+ if key in self._urlkeys:
+ values = ['ldap:///%s' % x for x in value_dict['values']]
+ else:
+ values = value_dict['values']
+ for value in values[:-1]:
+ rawaci += "%s || " % value
+ rawaci += values[-1]
+ rawaci += '"'
+ if self.verbose:
+ print("_format_term: %s" % rawaci)
+ return rawaci
+
+ def getRawAci(self):
+ """This method will rebuild an aci from the contents of the
acidata
+ dict found on the object.
+
+ :returns: An aci attribute string.
+ """
+
+ # Rebuild the aci from the .acidata.
+ rawaci = ''
+ # For each key in the outer segment
+ # Add a (key = val);. Depending on key format val:
+ for key in self._keys:
+ for value_dict in self.acidata[key]:
+ rawaci += '(%s %s)' % (key, self._format_term(key, value_dict))
+ # Now create the v3.0 aci part
+ rawaci += "(version 3.0; "
+ # This could be neater ...
+ rawaci += 'acl "%s";' %
self.acidata['acl'][0]['values'][0]
+ for key in ['allow', 'deny']:
+ if len(self.acidata[key]) > 0:
+ rawaci += '%s (' % key
+ for value in self.acidata[key][0]['values'][:-1]:
+ rawaci += '%s, ' % value
+ rawaci += '%s)' % self.acidata[key][0]['values'][-1]
+ rawaci += ('(%s);' % self.acidata["%s_raw_bindrules" %
+ key][0]['values'][-1])
+ rawaci += ")"
+ return rawaci
+
+ def _find_terms(self, aci):
+ if self.verbose:
+ print("_find_terms aci: %s" % aci)
+ print("_find_terms aci: %s" % type(aci))
+ lbr_list = []
+ rbr_list = []
+ depth = 0
+ for i, char in enumerate(aci):
+ if char == '(' and depth == 0:
+ lbr_list.append(i)
+ if char == '(':
+ depth += 1
+ if char == ')' and depth == 1:
+ rbr_list.append(i)
+ if char == ')':
+ depth -= 1
+ # Now build a set of terms.
+ terms = []
+ if self.verbose:
+ print("_find_terms lbr_list" % lbr_list)
+ print("_find_terms rbr_list" % rbr_list)
+ for lb, rb in zip(lbr_list, rbr_list):
+ terms.append(aci[lb + 1:rb])
+ if self.verbose:
+ print("_find_terms terms: %s" % terms)
+ return terms
+
+ def _parse_term(self, key, term):
+ wdict = {'values': [], 'equal': True}
+ # Nearly all terms are = seperated
+ # We make a dict that holds "equal" and an array of values
+ pre, val = term.split('=', 1)
+ val = val.replace('"', '')
+ if pre.strip() == '!':
+ wdict['equal'] = False
+ else:
+ wdict['equal'] = True
+ wdict['values'] = val.split('||')
+ if key in self._urlkeys:
+ # We could replace ldap:/// in some attrs?
+ wdict['values'] = ([x.replace('ldap:///', '')
+ for x in wdict['values']])
+ wdict['values'] = [x.strip() for x in wdict['values']]
+
+ if self.verbose:
+ print("_parse_term: %s" % wdict)
+ return wdict
+
+ def _parse_bind_rules(self, subterm):
+
+ # First, determine if there are extraneous braces wrapping the term.
+ subterm = subterm.strip()
+ if subterm[0] == '(' and subterm[-1] == ')':
+ subterm = subterm[1:-1]
+ terms = subterm.split('and')
+ """
+ We could parse everything into nice structures, and then work with
+ them. Or we can just leave the bind rule alone, as a string. Let
+ the human do it. It comes down to cost versus reward.
+ """
+
+ if self.verbose:
+ print("_parse_bind_rules: %s" % subterm)
+
+ return [subterm]
+
+ def _parse_version_3_0(self, rawacipart, data):
+ # We have to do this because it's not the same as other term formats.
+ terms = []
+ bindrules = []
+ interms = rawacipart.split(';')
+ interms = [x.strip() for x in interms]
+ for iwork in interms:
+ for j in self._v3keys + self._v3innerkeys:
+ if iwork.startswith(j) and j == 'acl':
+ t = iwork.split(' ', 1)[1]
+ t = t.replace('"', '')
+ data[j].append({'values': [t]})
+ if iwork.startswith(j) and (j == 'allow' or j ==
'deny'):
+ first = iwork.index('(') + 1
+ second = iwork.index(')', first)
+ # This could likely be neater ...
+ data[j].append(
+ {'values': [x.strip()
+ for x in iwork[first:second].split(',')]})
+ subterm = iwork[second + 1:]
+ data["%s_raw_bindrules" % j].append({
+ 'values': self._parse_bind_rules(subterm)
+ })
+
+ if self.verbose:
+ print("_parse_version_3_0: %s" % terms)
+ return terms
+
+ def _parse_aci(self, rawaci):
+ aci = rawaci
+ depth = 0
+ data = {
+ 'rawaci': rawaci,
+ 'allow_raw_bindrules': [],
+ 'deny_raw_bindrules': [],
+ }
+ for k in self._keys + self._v3keys:
+ data[k] = []
+ # We need to get a list of all the depth 0 ( and )
+ terms = self._find_terms(aci)
+
+ while len(terms) > 0:
+ work = terms.pop()
+ for k in self._keys + self._v3keys + self._v3innerkeys:
+ if work.startswith(k):
+ aci = work.replace(k, '', 1)
+ if k == 'version 3.0;':
+ """
+ We pop more inner terms out, but we don't need to
+ parse them "now" they get added to the queue
+ """
+ terms += self._parse_version_3_0(aci, data)
+ continue
+ data[k].append(self._parse_term(k, aci))
+ break
+ if self.verbose:
+ print("_parse_aci: %s" % data)
+ return data
diff --git a/src/lib389/lib389/_ldifconn.py b/src/lib389/lib389/_ldifconn.py
new file mode 100644
index 0000000..b6ef5a7
--- /dev/null
+++ b/src/lib389/lib389/_ldifconn.py
@@ -0,0 +1,53 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldif
+import six
+from lib389._entry import Entry
+from lib389.utils import normalizeDN
+
+__all__ = ['LDIFConn']
+
+
+class LDIFConn(ldif.LDIFParser):
+ def __init__(
+ self,
+ input_file,
+ ignored_attr_types=None, max_entries=0, process_url_schemes=None
+ ):
+ """
+ See LDIFParser.__init__()
+
+ Additional Parameters:
+ all_records
+ List instance for storing parsed records
+ """
+ self.dndict = {} # maps dn to Entry
+ self.dnlist = [] # contains entries in order read
+ myfile = input_file
+ if isinstance(input_file, six.string_types):
+ myfile = open(input_file, "r")
+ ldif.LDIFParser.__init__(self, myfile, ignored_attr_types,
+ max_entries, process_url_schemes)
+ self.parse()
+ if isinstance(input_file, six.string_types):
+ myfile.close()
+
+ def handle(self, dn, entry):
+ """
+ Append single record to dictionary of all records.
+ """
+ if not dn:
+ dn = ''
+ newentry = Entry((dn, entry))
+ self.dndict[normalizeDN(dn)] = newentry
+ self.dnlist.append(newentry)
+
+ def get(self, dn):
+ ndn = normalizeDN(dn)
+ return self.dndict.get(ndn, Entry(None))
diff --git a/src/lib389/lib389/_mapped_object.py b/src/lib389/lib389/_mapped_object.py
new file mode 100644
index 0000000..f73cb25
--- /dev/null
+++ b/src/lib389/lib389/_mapped_object.py
@@ -0,0 +1,840 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+import ldap.dn
+from ldap import filter as ldap_filter
+import logging
+from functools import partial
+
+from lib389._entry import Entry
+from lib389._constants import DIRSRV_STATE_ONLINE
+from lib389.utils import (
+ ensure_bytes, ensure_str, ensure_int, ensure_list_bytes, ensure_list_str,
+ ensure_list_int
+ )
+
+# This function filter and term generation provided thanks to
+# The University of Adelaide. <william(a)adelaide.edu.au>
+
+
+def _term_gen(term):
+ while True:
+ yield term
+
+
+def _gen(op=None, extra=None):
+ filt = ''
+ if type(extra) == list:
+ for ext in extra:
+ filt += ext
+ elif type(extra) == str:
+ filt += extra
+ if filt != '':
+ filt = '(%s%s)' % (op, filt)
+ return filt
+
+
+def _gen_and(extra=None):
+ return _gen('&', extra)
+
+
+def _gen_or(extra=None):
+ return _gen('|', extra)
+
+
+def _gen_not(extra=None):
+ return _gen('!', extra)
+
+
+def _gen_filter(attrtypes, values, extra=None):
+ filt = ''
+ for attr, value in zip(attrtypes, values):
+ if attr is not None and value is not None:
+ filt += '(%s=%s)' % (attr, ldap_filter.escape_filter_chars(value))
+ if extra is not None:
+ filt += '{FILT}'.format(FILT=extra)
+ return filt
+
+
+class DSLogging(object):
+ """The benefit of this is automatic name detection, and correct
application
+ of level and verbosity to the object.
+
+ :param verbose: False by default
+ :type verbose: bool
+ """
+
+ def __init__(self, verbose=False):
+ # Maybe we can think of a way to make this display the instance name or
__unicode__?
+ self._log = logging.getLogger(type(self).__name__)
+ if verbose:
+ self._log.setLevel(logging.DEBUG)
+ else:
+ self._log.setLevel(logging.INFO)
+
+
+class DSLdapObject(DSLogging):
+ """A single instance of DSLdapObjects
+
+ :param instance: A instance
+ :type instance: lib389.DirSrv
+ :param dn: Entry DN
+ :type dn: str
+ :param batch: Not implemented
+ :type batch: bool
+ """
+
+ # TODO: Automatically create objects when they are requested to have properties
added
+ def __init__(self, instance, dn=None, batch=False):
+ self._instance = instance
+ super(DSLdapObject, self).__init__(self._instance.verbose)
+ # This allows some factor objects to be overriden
+ self._dn = None
+ if dn is not None:
+ self._dn = ensure_str(dn)
+
+ self._batch = batch
+ self._protected = True
+ # Used in creation
+ self._create_objectclasses = []
+ self._rdn_attribute = None
+ self._must_attributes = None
+ # attributes, we don't want to compare
+ self._compare_exclude = ['entryid']
+ self._lint_functions = None
+ self._server_controls = None
+ self._client_controls = None
+
+ def __unicode__(self):
+ val = self._dn
+ if self._rdn_attribute:
+ # What if the rdn is multi value and we don't get the primary ....
ARGHHH
+ val = self.get_attr_val(self._rdn_attribute)
+ return ensure_str(val)
+
+ def __str__(self):
+ return self.__unicode__()
+
+ def raw_entry(self):
+ """Get an Entry object
+
+ :returns: Entry object
+ """
+
+ return self._instance.search_ext_s(self._dn, ldap.SCOPE_BASE,
attrlist=["*"], serverctrls=self._server_controls,
clientctrls=self._client_controls)[0]
+
+ def exists(self):
+ """Check if the entry exists
+
+ :returns: True if it exists
+ """
+
+ try:
+ self._instance.search_ext_s(self._dn, ldap.SCOPE_BASE, attrsonly=1,
serverctrls=self._server_controls, clientctrls=self._client_controls)
+ except ldap.NO_SUCH_OBJECT:
+ return False
+
+ return True
+
+ def display(self):
+ """Get an entry but represent it as a string LDIF
+
+ :returns: LDIF formatted string
+ """
+
+ e = self._instance.search_ext_s(self._dn, ldap.SCOPE_BASE,
attrlist=["*"], serverctrls=self._server_controls,
clientctrls=self._client_controls)[0]
+ return e.__repr__()
+
+ def display_attr(self, attr):
+ """Get all values of given attribute - 'attr: value'
+
+ :returns: Formatted string
+ """
+
+ out = ""
+ for v in self.get_attr_vals_utf8(attr):
+ out += "%s: %s\n" % (attr, v)
+ return out
+
+ def _jsonify(self, fn, *args, **kwargs):
+ # This needs to map all the values to ensure_str
+ attrs = fn(*args, **kwargs)
+ str_attrs = {}
+ for k in attrs:
+ str_attrs[ensure_str(k)] = ensure_list_str(attrs[k])
+
+ response = { "dn": ensure_str(self._dn), "attrs" : str_attrs
}
+ print('json response')
+ print(response)
+
+ return response
+
+ def __getattr__(self, name):
+ """This enables a bit of magic to allow us to wrap any function
ending with
+ _json to it's form without json, then transformed. It means your function
+ *must* return it's values as a dict of:
+
+ { attr : [val, val, ...], attr : [], ... }
+ to be supported.
+ """
+
+ if (name.endswith('_json')):
+ int_name = name.replace('_json', '')
+ pfunc = partial(self._jsonify, fn=getattr(self, int_name))
+ return pfunc
+
+ # We make this a property so that we can over-ride dynamically if needed
+ @property
+ def dn(self):
+ """Get an object DN
+
+ :returns: DN
+ """
+
+ return self._dn
+
+ @property
+ def rdn(self):
+ """Get an object RDN
+
+ :returns: RDN
+ """
+
+ # How can we be sure this returns the primary one?
+ return ensure_str(self.get_attr_val(self._rdn_attribute))
+
+ def present(self, attr, value=None):
+ """Assert that some attr, or some attr / value exist on the
entry.
+
+ :param attr: an attribute name
+ :type attr: str
+ :param value: an attribute value
+ :type value: str
+
+ :returns: True if attr is present
+ """
+
+ if self._instance.state != DIRSRV_STATE_ONLINE:
+ raise ValueError("Invalid state. Cannot get presence on instance that is
not ONLINE")
+ self._log.debug("%s present(%r) %s" % (self._dn, attr, value))
+
+ e = self._instance.search_ext_s(self._dn, ldap.SCOPE_BASE, attrlist=[attr, ],
serverctrls=self._server_controls, clientctrls=self._client_controls)[0]
+ if value is None:
+ return e.hasAttr(attr)
+ else:
+ return e.hasValue(attr, value)
+
+ def add(self, key, value):
+ """Add an attribute with a value
+
+ :param key: an attribute name
+ :type key: str
+ :param value: an attribute value
+ :type value: str
+ """
+
+ self.set(key, value, action=ldap.MOD_ADD)
+
+ # Basically what it means;
+ def replace(self, key, value):
+ """Replace an attribute with a value
+
+ :param key: an attribute name
+ :type key: str
+ :param value: an attribute value
+ :type value: str
+ """
+ self.set(key, value, action=ldap.MOD_REPLACE)
+
+ # This needs to work on key + val, and key
+ def remove(self, key, value):
+ """Remove a value defined by key
+
+ :param key: an attribute name
+ :type key: str
+ :param value: an attribute value
+ :type value: str
+ """
+
+ # Do a mod_delete on the value.
+ self.set(key, value, action=ldap.MOD_DELETE)
+
+ def remove_all(self, key):
+ """Remove all values defined by key (if possible).
+
+ If an attribute is multi-valued AND required all values except one will
+ be deleted.
+
+ :param key: an attribute name
+ :type key: str
+ """
+
+ for val in self.get_attr_vals(key):
+ self.remove(key, val)
+
+ # maybe this could be renamed?
+ def set(self, key, value, action=ldap.MOD_REPLACE):
+ """Perform a specified action on a key with value
+
+ :param key: an attribute name
+ :type key: str
+ :param value: an attribute value
+ :type value: str
+ :param action: - ldap.MOD_REPLACE - by default
+ - ldap.MOD_ADD
+ - ldap.MOD_DELETE
+ :type action: int
+
+ :returns: result of modify_s operation
+ :raises: ValueError - if instance is not online
+ """
+
+ self._log.debug("%s set(%r, %r)" % (self._dn, key, value))
+ if self._instance.state != DIRSRV_STATE_ONLINE:
+ raise ValueError("Invalid state. Cannot set properties on instance that
is not ONLINE.")
+
+ if isinstance(value, list):
+ # value = map(lambda x: ensure_bytes(x), value)
+ value = ensure_list_bytes(value)
+ elif value is not None:
+ value = [ensure_bytes(value)]
+
+ if self._batch:
+ pass
+ else:
+ return self._instance.modify_ext_s(self._dn, [(action, key, value)],
serverctrls=self._server_controls, clientctrls=self._client_controls)
+
+ def apply_mods(self, mods):
+ """Perform modification operation using several mods at once
+
+ :param mods: [(action, key, value),]
+ :type mods: list of tuples
+ :raises: ValueError - if a provided mod op is invalid
+ """
+
+ mod_list = []
+ for mod in mods:
+ if len(mod) < 2:
+ # Error
+ raise ValueError('Not enough arguments in the mod op')
+ elif len(mod) == 2: # no action
+ action = ldap.MOD_REPLACE
+ key, value = mod
+ elif len(mod) == 3:
+ action, key, value = mod
+ if action != ldap.MOD_REPLACE or \
+ action != ldap.MOD_ADD or \
+ action != ldap.MOD_DELETE:
+ raise ValueError('Invalid mod action(%s)' % str(action))
+ else:
+ # Error too many items
+ raise ValueError('Too many arguments in the mod op')
+
+ if isinstance(value, list):
+ value = ensure_list_bytes(value)
+ else:
+ value = [ensure_bytes(value)]
+
+ mod_list.append((action, key, value))
+ return self._instance.modify_ext_s(self._dn, mod_list,
serverctrls=self._server_controls, clientctrls=self._client_controls)
+
+ @classmethod
+ def compare(cls, obj1, obj2):
+ """Compare if two RDN objects have same attributes and values.
+
+ This comparison is a loose comparison, not a strict one i.e. "this object
*is* this other object"
+ It will just check if the attributes are same.
+ 'nsUniqueId' attribute is not checked intentionally because we want to
compare arbitrary objects
+ i.e they may have different 'nsUniqueId' but same attributes.
+
+ Example::
+
+ cn=user1,ou=a
+ cn=user1,ou=b
+
+ Comparision of these two objects should result in same, even though their
'nsUniqueId' attribute differs.
+
+ :param obj1: An entry to check
+ :type obj1: lib389._mapped_object.DSLdapObject
+ :param obj2: An entry to check
+ :type obj2: lib389._mapped_object.DSLdapObject
+ :returns: True if objects have same attributes else returns False
+ :raises: ValueError - if obj1 or obj2 don't inherit DSLdapObject
+ """
+
+ # ensuring both the objects are RDN objects
+ if not issubclass(type(obj1), DSLdapObject) or not issubclass(type(obj2),
DSLdapObject):
+ raise ValueError("Invalid arguments: Expecting object types that
inherits 'DSLdapObject' class")
+ # check if RDN of objects is same
+ if obj1.rdn != obj2.rdn:
+ return False
+ obj1_attrs = obj1.get_compare_attrs()
+ obj2_attrs = obj2.get_compare_attrs()
+ # Bail fast if the keys don't match
+ if set(obj1_attrs.keys()) != set(obj2_attrs.keys()):
+ return False
+ # Check the values of each key
+ # using obj1_attrs.keys() because obj1_attrs.iterkleys() is not supported in
python3
+ for key in obj1_attrs.keys():
+ if set(obj1_attrs[key]) != set(obj2_attrs[key]):
+ return False
+ return True
+
+ def get_compare_attrs(self):
+ """Get a dictionary having attributes to be compared
+ i.e. excluding self._compare_exclude
+ """
+
+ self._log.debug("%s get_compare_attrs" % (self._dn))
+ all_attrs_dict = self.get_all_attrs()
+ # removing _compate_exclude attrs from all attrs
+ compare_attrs = set(all_attrs_dict.keys()) - set(self._compare_exclude)
+ compare_attrs_dict = {attr:all_attrs_dict[attr] for attr in compare_attrs}
+ return compare_attrs_dict
+
+ def get_all_attrs(self):
+ """Get a dictionary having all the attributes of the entry
+
+ :returns: Dict with real attributes and operational attributes
+ """
+
+ self._log.debug("%s get_all_attrs" % (self._dn))
+ if self._instance.state != DIRSRV_STATE_ONLINE:
+ raise ValueError("Invalid state. Cannot get properties on instance that
is not ONLINE")
+ else:
+ # retrieving real(*) and operational attributes(+)
+ attrs_entry = self._instance.search_ext_s(self._dn, ldap.SCOPE_BASE,
attrlist=["*", "+"], serverctrls=self._server_controls,
clientctrls=self._client_controls)[0]
+ # getting dict from 'entry' object
+ attrs_dict = attrs_entry.data
+ return attrs_dict
+
+ def get_attrs_vals(self, keys):
+ self._log.debug("%s get_attrs_vals(%r)" % (self._dn, keys))
+ if self._instance.state != DIRSRV_STATE_ONLINE:
+ raise ValueError("Invalid state. Cannot get properties on instance that
is not ONLINE")
+ else:
+ entry = self._instance.search_ext_s(self._dn, ldap.SCOPE_BASE, attrlist=keys,
serverctrls=self._server_controls, clientctrls=self._client_controls)[0]
+ return entry.getValuesSet(keys)
+
+ def get_attr_vals(self, key):
+ self._log.debug("%s get_attr_vals(%r)" % (self._dn, key))
+ # We might need to add a state check for NONE dn.
+ if self._instance.state != DIRSRV_STATE_ONLINE:
+ raise ValueError("Invalid state. Cannot get properties on instance that
is not ONLINE")
+ # In the future, I plan to add a mode where if local == true, we
+ # can use get on dse.ldif to get values offline.
+ else:
+ # It would be good to prevent the entry code intercepting this ....
+ # We have to do this in this method, because else we ignore the scope base.
+ entry = self._instance.search_ext_s(self._dn, ldap.SCOPE_BASE,
attrlist=[key], serverctrls=self._server_controls, clientctrls=self._client_controls)[0]
+ return entry.getValues(key)
+
+ def get_attr_val(self, key):
+ self._log.debug("%s getVal(%r)" % (self._dn, key))
+ # We might need to add a state check for NONE dn.
+ if self._instance.state != DIRSRV_STATE_ONLINE:
+ raise ValueError("Invalid state. Cannot get properties on instance that
is not ONLINE")
+ # In the future, I plan to add a mode where if local == true, we
+ # can use get on dse.ldif to get values offline.
+ else:
+ entry = self._instance.search_ext_s(self._dn, ldap.SCOPE_BASE,
attrlist=[key], serverctrls=self._server_controls, clientctrls=self._client_controls)[0]
+ return entry.getValue(key)
+
+ def get_attr_val_bytes(self, key):
+ """Get a single attribute value from the entry in bytes type
+
+ :param key: An attribute name
+ :type key: str
+ :returns: A single bytes value
+ :raises: ValueError - if instance is offline
+ """
+
+ return ensure_bytes(self.get_attr_val(key))
+
+ def get_attr_vals_bytes(self, key):
+ """Get attribute values from the entry in bytes type
+
+ :param key: An attribute name
+ :type key: str
+ :returns: A single bytes value
+ :raises: ValueError - if instance is offline
+ """
+
+ return ensure_list_bytes(self.get_attr_vals(key))
+
+ def get_attr_val_utf8(self, key):
+ """Get a single attribute value from the entry in utf8 type
+
+ :param key: An attribute name
+ :type key: str
+ :returns: A single bytes value
+ :raises: ValueError - if instance is offline
+ """
+
+ return ensure_str(self.get_attr_val(key))
+
+ def get_attr_vals_utf8(self, key):
+ """Get attribute values from the entry in utf8 type
+
+ :param key: An attribute name
+ :type key: str
+ :returns: A single bytes value
+ :raises: ValueError - if instance is offline
+ """
+
+ return ensure_list_str(self.get_attr_vals(key))
+
+ def get_attr_val_int(self, key):
+ """Get a single attribute value from the entry in int type
+
+ :param key: An attribute name
+ :type key: str
+ :returns: A single bytes value
+ :raises: ValueError - if instance is offline
+ """
+
+ return ensure_int(self.get_attr_val(key))
+
+ def get_attr_vals_int(self, key):
+ """Get attribute values from the entry in int type
+
+ :param key: An attribute name
+ :type key: str
+ :returns: A single bytes value
+ :raises: ValueError - if instance is offline
+ """
+
+ return ensure_list_int(self.get_attr_vals(key))
+
+ # Duplicate, but with many values. IE a dict api.
+ # This
+ def add_values(self, values):
+ pass
+
+ def replace_values(self, values):
+ pass
+
+ def set_values(self, values, action=ldap.MOD_REPLACE):
+ pass
+
+ # If the account can be bound to, this will attempt to do so. We don't check
+ # for exceptions, just pass them back!
+ def bind(self, password=None, *args, **kwargs):
+ """Open a new connection and bind with the entry.
+ You can pass arguments that will be passed to openConnection.
+
+ :param password: An entry password
+ :type password: str
+ :returns: Connection with a binding as the entry
+ """
+
+ conn = self._instance.openConnection(*args, **kwargs)
+ conn.simple_bind_s(self.dn, password)
+ return conn
+
+ def delete(self):
+ """Deletes the object defined by self._dn.
+ This can be changed with the self._protected flag!
+ """
+
+ self._log.debug("%s delete" % (self._dn))
+ if not self._protected:
+ # Is there a way to mark this as offline and kill it
+ self._instance.delete_ext_s(self._dn, serverctrls=self._server_controls,
clientctrls=self._client_controls)
+
+ def _validate(self, rdn, properties, basedn):
+ """Used to validate a create request.
+ This way, it can be over-ridden without affecting
+ the create types.
+
+ It also checks that all the values in _must_attribute exist
+ in some form in the dictionary.
+
+ It has the useful trick of returning the dn, so subtypes
+ can use extra properties to create the dn's here for this.
+ """
+
+ if properties is None:
+ raise ldap.UNWILLING_TO_PERFORM('Invalid request to create. Properties
cannot be None')
+ if type(properties) != dict:
+ raise ldap.UNWILLING_TO_PERFORM("properties must be a dictionary")
+
+ # I think this needs to be made case insensitive
+ # How will this work with the dictionary?
+ for attr in self._must_attributes:
+ if properties.get(attr, None) is None:
+ raise ldap.UNWILLING_TO_PERFORM('Attribute %s must not be None' %
attr)
+
+ # Make sure the naming attribute is present
+ if properties.get(self._rdn_attribute, None) is None and rdn is None:
+ raise ldap.UNWILLING_TO_PERFORM('Attribute %s must not be None or rdn
provided' % self._rdn_attribute)
+
+ # Great! Now, lets fix up our types
+ for k, v in properties.items():
+ if isinstance(v, list):
+ # Great!
+ pass
+ else:
+ # Turn it into a list instead.
+ properties[k] = [v,]
+
+ # This change here, means we can pre-load a full dn to _dn, or we can
+ # accept based on the rdn
+ tdn = self._dn
+
+ if tdn is None:
+ if basedn is None:
+ raise ldap.UNWILLING_TO_PERFORM('Invalid request to create. basedn
cannot be None')
+
+ if properties.get(self._rdn_attribute, None) is not None:
+ # Favour the value in the properties dictionary
+ v = properties.get(self._rdn_attribute)
+ rdn = ensure_str(v[0])
+
+ erdn = ensure_str(ldap.dn.escape_dn_chars(rdn))
+ self._log.debug("Using first property %s: %s as rdn" %
(self._rdn_attribute, erdn))
+ # Now we compare. If we changed this value, we have to put it back to
make the properties complete.
+ if erdn != rdn:
+ properties[self._rdn_attribute].append(erdn)
+
+ tdn = ensure_str('%s=%s,%s' % (self._rdn_attribute, erdn,
basedn))
+
+ # We may need to map over the data in the properties dict to satisfy python-ldap
+ str_props = {}
+ for k, v in properties.items():
+ str_props[k] = ensure_list_bytes(v)
+ #
+ # Do we need to do extra dn validation here?
+ return (tdn, str_props)
+
+ def create(self, rdn=None, properties=None, basedn=None):
+ """Add a new entry
+
+ :param rdn: RDN of the new entry
+ :type rdn: str
+ :param properties: Attributes for the new entry
+ :type properties: dict
+ :param basedn: Base DN of the new entry
+ :type rdn: str
+
+ :returns: DSLdapObject of the created entry
+ """
+
+ assert(len(self._create_objectclasses) > 0)
+ basedn = ensure_str(basedn)
+ self._log.debug('Creating "%s" under %s : %s' % (rdn, basedn,
properties))
+ # Add the objectClasses to the properties
+ (dn, valid_props) = self._validate(rdn, properties, basedn)
+ # Check if the entry exists or not? .add_s is going to error anyway ...
+ self._log.debug('Validated dn %s : valid_props %s' % (dn, valid_props))
+
+ e = Entry(dn)
+ e.update({'objectclass': ensure_list_bytes(self._create_objectclasses)})
+ e.update(valid_props)
+ # We rely on exceptions here to indicate failure to the parent.
+ self._log.debug('Creating entry %s : %s' % (dn, e))
+ self._instance.add_ext_s(e, serverctrls=self._server_controls,
clientctrls=self._client_controls)
+ # If it worked, we need to fix our instance dn
+ self._dn = dn
+ return self
+
+ def lint(self):
+ """Override this to create a linter for a type. This means that we
can detect
+ and report common administrative errors in the server from our cli and
+ rest tools.
+
+ The structure of a result is::
+
+ {
+ dsle: '<identifier>'. dsle == ds lint error. Will be a code
unique to
+ this module for the error, IE DSBLE0001.
+ severity: '[HIGH:MEDIUM:LOW]'. severity of the error.
+ items: '(dn,dn,dn)'. List of affected DNs or names.
+ detail: 'msg ...'. An explination of the error.
+ fix: 'msg ...'. Steps to resolve the error.
+ }
+
+ :returns: An array of these dicts, on None if there are no errors.
+ """
+
+ if not self._lint_functions:
+ return None
+ results = []
+ for fn in self._lint_functions:
+ result = fn()
+ if result:
+ results.append(result)
+ return results
+
+# A challenge of this, is how do we manage indexes? They have two naming attribunes....
+
+class DSLdapObjects(DSLogging):
+ """The object represents the next idea: "Everything is an
instance of something
+ that exists in this way", i.e. we unite LDAP entries by some
+ set of parameters with the object.
+
+ :param instance: A instance
+ :type instance: lib389.DirSrv
+ :param batch: Not implemented
+ :type batch: bool
+ """
+
+ def __init__(self, instance, batch=False):
+ self._childobject = DSLdapObject
+ self._instance = instance
+ super(DSLdapObjects, self).__init__(self._instance.verbose)
+ self._objectclasses = []
+ self._filterattrs = []
+ self._list_attrlist = ['dn']
+ # Copy this from the child if we need.
+ self._basedn = ""
+ self._batch = batch
+ self._scope = ldap.SCOPE_SUBTREE
+ self._server_controls = None
+ self._client_controls = None
+
+ def _get_objectclass_filter(self):
+ return _gen_and(
+ _gen_filter(_term_gen('objectclass'), self._objectclasses)
+ )
+
+ def _entry_to_instance(self, dn=None, entry=None):
+ # Normally this won't be used. But for say the plugin type where we
+ # have "many" possible child types, this allows us to overload
+ # and select / return the right one through ALL our get/list/create
+ # functions with very little work on the behalf of the overloader
+ return self._childobject(instance=self._instance, dn=dn, batch=self._batch)
+
+ def list(self):
+ """Get a list of children entries (DSLdapObject, Replica, etc.)
using a base DN
+ and objectClasses of our object (DSLdapObjects, Replicas, etc.)
+
+ :returns: A list of children entries
+ """
+
+ # Filter based on the objectclasses and the basedn
+ insts = None
+ # This will yield and & filter for objectClass with as many terms as needed.
+ filterstr = self._get_objectclass_filter()
+ self._log.debug('list filter = %s' % filterstr)
+ try:
+ results = self._instance.search_ext_s(
+ base=self._basedn,
+ scope=self._scope,
+ filterstr=filterstr,
+ attrlist=self._list_attrlist,
+ serverctrls=self._server_controls, clientctrls=self._client_controls
+ )
+ # def __init__(self, instance, dn=None, batch=False):
+ insts = [self._entry_to_instance(dn=r.dn, entry=r) for r in results]
+ except ldap.NO_SUCH_OBJECT:
+ # There are no objects to select from, se we return an empty array
+ insts = []
+ return insts
+
+ def get(self, selector=[], dn=None):
+ """Get a child entry (DSLdapObject, Replica, etc.) with dn or
selector
+ using a base DN and objectClasses of our object (DSLdapObjects, Replicas, etc.)
+
+ :param dn: DN of wanted entry
+ :type dn: str
+ :param selector: An additional filter to objectClasses, i.e.
'backend_name'
+ :type dn: str
+
+ :returns: A child entry
+ """
+
+ results = []
+ if dn is not None:
+ results = self._get_dn(dn)
+ else:
+ results = self._get_selector(selector)
+
+ if len(results) == 0:
+ raise ldap.NO_SUCH_OBJECT("No object exists given the filter criteria
%s" % selector)
+ if len(results) > 1:
+ raise ldap.UNWILLING_TO_PERFORM("Too many objects matched selection
criteria %s" % selector)
+ return self._entry_to_instance(results[0].dn, results[0])
+
+ def _get_dn(self, dn):
+ # This will yield and & filter for objectClass with as many terms as needed.
+ filterstr = self._get_objectclass_filter()
+ self._log.debug('_gen_dn filter = %s' % filterstr)
+ return self._instance.search_ext_s(
+ base=dn,
+ scope=ldap.SCOPE_BASE,
+ filterstr=filterstr,
+ attrlist=self._list_attrlist,
+ serverctrls=self._server_controls, clientctrls=self._client_controls
+ )
+
+ def _get_selector(self, selector):
+ # Filter based on the objectclasses and the basedn
+ # Based on the selector, we should filter on that too.
+ # This will yield and & filter for objectClass with as many terms as needed.
+ filterstr=_gen_and([
+ self._get_objectclass_filter(),
+ _gen_or(
+ # This will yield all combinations of selector to filterattrs.
+ # This won't work with multiple values in selector (yet)
+ _gen_filter(self._filterattrs, _term_gen(selector))
+ ),
+ ])
+ self._log.debug('_gen_selector filter = %s' % filterstr)
+ return self._instance.search_ext_s(
+ base=self._basedn,
+ scope=self._scope,
+ filterstr=filterstr,
+ attrlist=self._list_attrlist,
+ serverctrls=self._server_controls, clientctrls=self._client_controls
+ )
+
+ def _validate(self, rdn, properties):
+ """Validate the factory part of the creation"""
+
+ if properties is None:
+ raise ldap.UNWILLING_TO_PERFORM('Invalid request to create. Properties
cannot be None')
+ if type(properties) != dict:
+ raise ldap.UNWILLING_TO_PERFORM("properties must be a dictionary")
+
+ # Get the rdn out of the properties if it's unset???
+ if rdn is None and self._rdn_attribute in properties:
+ # First see if we can get it from the properties.
+ trdn = properties.get(self._rdn_attribute)
+ if type(trdn) == str:
+ rdn = trdn
+ elif type(trdn) == list and len(trdn) != 1:
+ raise ldap.UNWILLING_TO_PERFORM("Cannot determine rdn %s from
properties. Too many choices" % (self._rdn_attribute))
+ elif type(trdn) == list:
+ rdn = trdn[0]
+ else:
+ raise ldap.UNWILLING_TO_PERFORM("Cannot determine rdn %s from
properties, Invalid type" % type(trdn))
+
+ return (rdn, properties)
+
+ def create(self, rdn=None, properties=None):
+ """Create an object under base DN of our entry
+
+ :param rdn: RDN of the new entry
+ :type rdn: str
+ :param properties: Attributes for the new entry
+ :type properties: dict
+
+ :returns: DSLdapObject of the created entry
+ """
+
+ # Should we inject the rdn to properties?
+ # This may not work in all cases, especially when we consider plugins.
+ #
+ co = self._entry_to_instance(dn=None, entry=None)
+ # Make the rdn naming attr avaliable
+ self._rdn_attribute = co._rdn_attribute
+ (rdn, properties) = self._validate(rdn, properties)
+ # Now actually commit the creation req
+ return co.create(rdn, properties, self._basedn)
diff --git a/src/lib389/lib389/_replication.py b/src/lib389/lib389/_replication.py
new file mode 100644
index 0000000..7d51577
--- /dev/null
+++ b/src/lib389/lib389/_replication.py
@@ -0,0 +1,202 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import time
+import datetime
+import logging
+import re
+
+log = logging.getLogger(__name__)
+
+
+class CSN(object):
+ """CSN is Change Sequence Number
+ csn.ts is the timestamp (time_t - seconds)
+ csn.seq is the sequence number (max 65535)
+ csn.rid is the replica ID of the originating master
+ csn.subseq is not currently used"""
+
+ csnpat = r'(.{8})(.{4})(.{4})(.{4})'
+ csnre = re.compile(csnpat)
+
+ def __init__(self, csnstr):
+ match = CSN.csnre.match(csnstr)
+ self.ts = 0
+ self.seq = 0
+ self.rid = 0
+ self.subseq = 0
+ if match:
+ self.ts = int(match.group(1), 16)
+ self.seq = int(match.group(2), 16)
+ self.rid = int(match.group(3), 16)
+ self.subseq = int(match.group(4), 16)
+ elif csnstr:
+ self.ts = 0
+ self.seq = 0
+ self.rid = 0
+ self.subseq = 0
+ log.info("%r is not a valid CSN" % csnstr)
+
+ def csndiff(self, oth):
+ return (oth.ts - self.ts,
+ oth.seq - self.seq,
+ oth.rid - self.rid,
+ oth.subseq - self.subseq)
+
+ def __cmp__(self, oth):
+ if self is oth:
+ return 0
+ (tsdiff, seqdiff, riddiff, subseqdiff) = self.csndiff(oth)
+
+ diff = tsdiff or seqdiff or riddiff or subseqdiff
+ ret = 0
+ if diff > 0:
+ ret = 1
+ elif diff < 0:
+ ret = -1
+ return ret
+
+ def __eq__(self, oth):
+ return cmp(self, oth) == 0
+
+ def diff2str(self, oth):
+ retstr = ''
+ diff = oth.ts - self.ts
+ if diff > 0:
+ td = datetime.timedelta(seconds=diff)
+ retstr = "is behind by %s" % td
+ elif diff < 0:
+ td = datetime.timedelta(seconds=-diff)
+ retstr = "is ahead by %s" % td
+ else:
+ diff = oth.seq - self.seq
+ if diff:
+ retstr = "seq differs by %d" % diff
+ elif self.rid != oth.rid:
+ retstr = "rid %d not equal to rid %d" % (self.rid, oth.rid)
+ else:
+ retstr = "equal"
+ return retstr
+
+ def __repr__(self):
+ return ("%s seq: %s rid: %s" % (time.strftime("%x %X",
time.localtime(self.ts)),
+ str(self.seq), str(self.rid)))
+
+ def __str__(self):
+ return self.__repr__()
+
+
+class RUV(object):
+ """RUV is Replica Update Vector
+ ruv.gen is the generation CSN
+ ruv.rid[1] through ruv.rid[N] are dicts - the number (1-N)
+ is the replica ID
+ ruv.rid[N][url] is the purl
+ ruv.rid[N][min] is the min csn
+ ruv.rid[N][max] is the max csn
+ ruv.rid[N][lastmod] is the last modified timestamp
+ example ruv attr:
+ nsds50ruv: {replicageneration} 3b0ebc7f000000010000
+ nsds50ruv: {replica 1 ldap://myhost:51010}
+ 3b0ebc9f000000010000 3b0ebef7000000010000
+ nsre_ruvplicaLastModified: {replica 1 ldap://myhost:51010}
+ 292398402093
+ if the try repl flag is true, if getting the ruv from the
+ suffix fails, try getting the ruv from the cn=replica entry
+ """
+
+ pre_gen = r'\{replicageneration\}\s+(\w+)'
+ re_gen = re.compile(pre_gen)
+ pre_ruv = r'\{replica\s+(\d+)\s+(.+?)\}\s*(\w*)\s*(\w*)'
+ re_ruv = re.compile(pre_ruv)
+
+ def __init__(self, ent):
+ # rid is a dict
+ # key is replica ID - val is dict of url, min csn, max csn
+ self.rid = {}
+ for item in ent.getValues('nsds50ruv'):
+ matchgen = RUV.re_gen.match(item)
+ matchruv = RUV.re_ruv.match(item)
+ if matchgen:
+ self.gen = CSN(matchgen.group(1))
+ elif matchruv:
+ rid = int(matchruv.group(1))
+ self.rid[rid] = {'url': matchruv.group(2),
+ 'min': CSN(matchruv.group(3)),
+ 'max': CSN(matchruv.group(4))}
+ else:
+ log.info("unknown RUV element %r" % item)
+ for item in ent.getValues('nsre_ruvplicaLastModified'):
+ matchruv = RUV.re_ruv.match(item)
+ if matchruv:
+ rid = int(matchruv.group(1))
+ self.rid[rid]['lastmod'] = int(matchruv.group(3), 16)
+ else:
+ log.info("unknown nsre_ruvplicaLastModified item %r" % item)
+
+ def __cmp__(self, oth):
+ if self is oth:
+ return 0
+ if not self:
+ return -1 # None is less than something
+ if not oth:
+ return 1 # something is greater than None
+ diff = cmp(self.gen, oth.gen)
+ if diff:
+ return diff
+ for rid in list(self.rid.keys()):
+ for item in ('max', 'min'):
+ csn = self.rid[rid][item]
+ csnoth = oth.rid[rid][item]
+ diff = cmp(csn, csnoth)
+ if diff:
+ return diff
+ return 0
+
+ def __eq__(self, oth):
+ return cmp(self, oth) == 0
+
+ def __str__(self):
+ ret = 'generation: %s\n' % self.gen
+ for rid in list(self.rid.keys()):
+ ret = ret + 'rid: %s url: %s min: [%s] max: [%s]\n' % \
+ (rid, self.rid[rid]['url'], self.rid[rid].get('min',
''),
+ self. rid[rid].get('max', ''))
+ return ret
+
+ def getdiffs(self, oth):
+ """Compare two ruvs and return the differences
+ returns a tuple - the first element is the
+ result of cmp() - the second element is a string"""
+ if self is oth:
+ return (0, "\tRUVs are the same")
+ if not self:
+ return (-1, "\tfirst RUV is empty")
+ if not oth:
+ return (1, "\tsecond RUV is empty")
+ diff = cmp(self.gen, oth.gen)
+ if diff:
+ return (diff, "\tgeneration [" + str(self.gen) +
+ "] not equal to [" + str(oth.gen) +
+ "]: likely not yet initialized")
+ retstr = ''
+ for rid in list(self.rid.keys()):
+ for item in ('max', 'min'):
+ csn = self.rid[rid][item]
+ csnoth = oth.rid[rid][item]
+ csndiff = cmp(csn, csnoth)
+ if csndiff:
+ if len(retstr):
+ retstr += "\n"
+ retstr += ("\trid %d %scsn %s\n\t[%s] vs [%s]" %
+ (rid, item, csn.diff2str(csnoth), csn, csnoth))
+ if not diff:
+ diff = csndiff
+ if not diff:
+ retstr = "\tup-to-date - RUVs are equal"
+ return (diff, retstr)
diff --git a/src/lib389/lib389/aci.py b/src/lib389/lib389/aci.py
new file mode 100644
index 0000000..1f0725a
--- /dev/null
+++ b/src/lib389/lib389/aci.py
@@ -0,0 +1,240 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""Aci class to help parse and create ACIs.
+
+You will access this via the Entry Class.
+"""
+
+import ldap
+
+from lib389._constants import *
+
+
+# Helpers to detect common patterns in aci
+def _aci_any_targetattr_ne(aci):
+ """Returns True if any of the targetattr types is a !=
type"""
+
+ potential = False
+ if 'targetattr' in aci.acidata:
+ for ta in aci.acidata['targetattr']:
+ if ta['equal'] is False:
+ # Got, it lets do this
+ potential = True
+ return potential
+
+
+class Aci(object):
+ """An object that helps to work with agreement entry
+
+ :param conn: An instance
+ :type conn: lib389.DirSrv
+ """
+
+ def __init__(self, conn):
+ self.conn = conn
+ self.log = conn.log
+
+ def list(self, basedn, scope=ldap.SCOPE_SUBTREE):
+ """List all acis in the directory server below the basedn confined
by
+ scope.
+
+ :param basedn: Base DN
+ :type basedn: str
+ :param scope: ldap.SCOPE_SUBTREE, ldap.SCOPE_BASE,
+ ldap.SCOPE_ONELEVEL, ldap.SCOPE_SUBORDINATE
+ :type scope: int
+
+ :returns: A list of EntryAci objects
+ """
+
+ acis = []
+ rawacientries = self.conn.search_s(basedn, scope, 'aci=*',
['aci'])
+ for rawacientry in rawacientries:
+ acis += rawacientry.getAcis()
+ return acis
+
+ def lint(self, basedn, scope=ldap.SCOPE_SUBTREE):
+ """Validate and check for potential aci issues.
+
+ Given a scope and basedn, this will retrieve all the aci's below.
+ A number of checks are then run on the aci in isolation, and
+ in groups.
+
+ :param basedn: Base DN
+ :type basedn: str
+ :param scope: ldap.SCOPE_SUBTREE, ldap.SCOPE_BASE,
+ ldap.SCOPE_ONELEVEL, ldap.SCOPE_SUBORDINATE
+ :type scope: int
+
+ :returns: A tuple of (bool, list( dict ))
+ - Bool represents if the acis pass or fail as a whole.
+ - The list contains a list of warnings about your acis.
+ - The dict is structured as::
+
+ {
+ name: "" # DSALEXXXX
+ severity: "" # LOW MEDIUM HIGH
+ detail: "" # explination
+ }
+ """
+
+ result = True
+ # Not thread safe!!!
+ self.warnings = []
+
+ acis = self.list(basedn, scope)
+ # Checks again "all acis" go here.
+ self._lint_dsale_0001_ne_internal(acis)
+ self._lint_dsale_0002_ne_mult_subtree(acis)
+ # checks again individual here
+
+ if len(self.warnings) > 0:
+ result = False
+
+ return (result, self.warnings)
+
+ def format_lint(self, warnings):
+ """Takes the array of warnings and returns a formatted string.
+
+ :param warnings: The array of warnings
+ :type warnings: dict
+
+ :returns: Formatted string or warnings
+ """
+
+ buf =
"-------------------------------------------------------------------------------"
+
+ for warning in warnings:
+ buf += """
+Directory Server Aci Lint Error: {DSALE}
+Severity: {SEVERITY}
+
+Affected Acis:
+{ACIS}
+
+Details: {DETAIL}
+
+Advice: {FIX}
+-------------------------------------------------------------------------------""".format(
+ DSALE=warning['dsale'],
+ SEVERITY=warning['severity'],
+ ACIS=warning['acis'],
+ DETAIL=warning['detail'],
+ FIX=warning['fix'],
+ )
+ return buf
+
+ # These are the aci lint checks.
+
+ def _lint_dsale_0001_ne_internal(self, acis):
+ """Check for the presence of "not equals" attributes
that will inadvertantly
+ allow the return / modification of internal attributes.
+ """
+
+ affected = []
+ for aci in acis:
+ if 'targetattr' in aci.acidata:
+ for ta in aci.acidata['targetattr']:
+ if ta['equal'] is False:
+ affected.append(aci.acidata['rawaci'])
+
+ if len(affected) > 0:
+ self.warnings.append(
+ {
+ 'dsale': 'DSALE0001',
+ 'severity': 'HIGH',
+ 'acis': "\n".join(affected),
+ 'detail': """
+An aci of the form "(targetAttr!="attr")" exists on your system. This
aci
+will internally be expanded to mean "all possible attributes including system,
+excluding the listed attributes".
+
+This may allow access to a bound user or anonymous to read more data about
+directory internals, including aci state or user limits. In the case of write
+acis it may allow a dn to set their own resource limits, unlock passwords or
+their own aci.
+
+The ability to change the aci on the object may lead to privilege escalation in
+some cases.
+ """,
+ 'fix': """
+Convert the aci to the form "(targetAttr="x || y || z")".
+ """
+ }
+ )
+
+ def _lint_dsale_0002_ne_mult_subtree(self, acis):
+ """This check will show pairs or more of aci that match the same
subtree
+ with a != rute. These can cause the other rule to be invalidated!
+ """
+
+ affected = []
+ for aci in acis:
+ # The aci has to be a NE, else don't bother checking
+ if not _aci_any_targetattr_ne(aci):
+ continue
+
+ affect = False
+ buf = "%s %s\n" % (aci.entry.dn, aci.acidata['rawaci'])
+ for aci_inner in acis:
+
+ if aci_inner == aci:
+ # Don't compare to self!
+ continue
+ # Check the inner is a not equal also
+ if not _aci_any_targetattr_ne(aci_inner):
+ continue
+
+ # Check if the dn is a substring, ie child, or equal.
+ if aci.entry.dn.endswith(aci_inner.entry.dn):
+ # alias the allow rules
+ allow_inner =
set(aci_inner.acidata['allow'][0]['values'])
+ allow_outer = set(aci.acidata['allow'][0]['values'])
+ if len(allow_inner & allow_outer):
+ buf += "|- %s %s\n" % (aci_inner.entry.dn,
aci_inner.acidata['rawaci'])
+ affect = True
+
+ if affect:
+ affected.append(buf)
+
+ if len(affected) > 0:
+ self.warnings.append(
+ {
+ 'dsale': 'DSALE0002',
+ 'severity': 'HIGH',
+ 'acis': "\n".join(affected),
+ 'detail': """
+Acis on your system exist which are both not equals targetattr, and overlap in
+scope.
+
+The way that directory server processes these, is to invert them to to white
+lists, then union the results.
+
+As a result, these acis *may* allow access to the attributes you want them to
+exclude.
+
+Consider:
+
+aci: (targetattr !="cn")(version 3.0;acl "Self write all but
cn";allow (write)
+ (userdn = "ldap:///self");)
+aci: (targetattr !="sn")(version 3.0;acl "Self write all but
sn";allow (write)
+ (userdn = "ldap:///self");)
+
+This combination allows self write to *all* attributes within the subtree.
+
+In cases where the target is members of a group, it may allow a member who is
+within two groups to have elevated privilege.
+ """,
+ 'fix': """
+Convert the aci to the form "(targetAttr="x || y || z")".
+
+Prevent the acis from overlapping, and have them on unique subtrees.
+ """
+ }
+ )
diff --git a/src/lib389/lib389/agreement.py b/src/lib389/lib389/agreement.py
new file mode 100644
index 0000000..822f9a3
--- /dev/null
+++ b/src/lib389/lib389/agreement.py
@@ -0,0 +1,756 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+import re
+import time
+import six
+
+from lib389._constants import *
+from lib389.properties import *
+from lib389._entry import FormatDict
+from lib389.utils import normalizeDN, ensure_bytes, ensure_str, ensure_dict_str
+from lib389 import Entry, DirSrv, NoSuchEntryError, InvalidArgumentError
+
+
+class Agreement(object):
+ """An object that helps to work with agreement entry
+
+ :param conn: An instance
+ :type conn: lib389.DirSrv
+ """
+
+ ALWAYS = '0000-2359 0123456'
+ NEVER = '2358-2359 0'
+
+ proxied_methods = 'search_s getEntry'.split()
+
+ def __init__(self, conn):
+ self.conn = conn
+ self.log = conn.log
+
+ def __getattr__(self, name):
+ if name in Agreement.proxied_methods:
+ return DirSrv.__getattr__(self.conn, name)
+
+ def status(self, agreement_dn, just_status=False):
+ """Get a formatted string with the replica status
+
+ :param agreement_dn: DN of the replica agreement
+ :type agreement_dn: str
+ :param just_status: If True, returns just status
+ :type just_status: bool
+
+ :returns: str -- See below
+ :raises: NoSuchEntryError - if agreement_dn is an unknown entry
+
+ :example:
+ ::
+
+ Status for meTo_localhost.localdomain:50389 agmt
+ localhost.localdomain:50389
+ Update in progress: TRUE
+ Last Update Start: 20131121132756Z
+ Last Update End: 0
+ Num. Changes Sent: 1:10/0
+ Num. changes Skipped: None
+ Last update Status: 0 Replica acquired successfully:
+ Incremental update started
+ Init in progress: None
+ Last Init Start: 0
+ Last Init End: 0
+ Last Init Status: None
+ Reap Active: 0
+ """
+
+ attrlist = ['cn', 'nsds5BeginReplicaRefresh',
'nsds5ReplicaRoot',
+ 'nsds5replicaUpdateInProgress',
+ 'nsds5ReplicaLastInitStatus',
'nsds5ReplicaLastInitStart',
+ 'nsds5ReplicaLastInitEnd', 'nsds5replicaReapActive',
+ 'nsds5replicaLastUpdateStart',
'nsds5replicaLastUpdateEnd',
+ 'nsds5replicaChangesSentSinceStartup',
+ 'nsds5replicaLastUpdateStatus',
+ 'nsds5replicaChangesSkippedSinceStartup',
+ 'nsds5ReplicaHost', 'nsds5ReplicaPort',
+ 'nsds5ReplicaEnabled', 'nsds5AgmtMaxCSN']
+ try:
+ ent = self.conn.getEntry(
+ agreement_dn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist)
+ except NoSuchEntryError:
+ raise NoSuchEntryError(
+ "Error reading status from agreement", agreement_dn)
+ else:
+ status = self.conn.getReplAgmtStatus(ent)
+ if just_status:
+ return status
+
+ retstr = (
+ "Status for %(cn)s agmt %(nsDS5ReplicaHost)s:"
+ "%(nsDS5ReplicaPort)s" "\n"
+ "Replica Enabled: %(nsds5ReplicaEnabled)s" "\n"
+ "Agreement maxCSN: %(nsds5AgmtMaxCSN)s" "\n"
+ "Update in progress: %(nsds5replicaUpdateInProgress)s"
"\n"
+ "Last Update Start: %(nsds5replicaLastUpdateStart)s"
"\n"
+ "Last Update End: %(nsds5replicaLastUpdateEnd)s"
"\n"
+ "Num. Changes Sent: %(nsds5replicaChangesSentSinceStartup)s"
+ "\n"
+ "Num. changes Skipped: %(nsds5replicaChangesSkippedSince"
+ "Startup)s" "\n"
+ "Last update Status: %(nsds5replicaLastUpdateStatus)s"
"\n"
+ "Init in progress: %(nsds5BeginReplicaRefresh)s"
"\n"
+ "Last Init Start: %(nsds5ReplicaLastInitStart)s"
"\n"
+ "Last Init End: %(nsds5ReplicaLastInitEnd)s" "\n"
+ "Last Init Status: %(nsds5ReplicaLastInitStatus)s"
"\n"
+ "Reap Active: %(nsds5ReplicaReapActive)s" "\n"
+ )
+ # FormatDict manages missing fields in string formatting
+ entry_data = ensure_dict_str(ent.data)
+ result = retstr % FormatDict(entry_data)
+ result += "Replication Status: %s\n" % status
+ return result
+
+ def _check_interval(self, interval):
+ """Check the interval for schedule replication is valid:
+ HH [0..23]
+ MM [0..59]
+ DAYS [0-6]{1,7}
+
+ :param interval: - 'HHMM-HHMM D+' With D=[0123456]+
+ - Agreement.ALWAYS
+ - Agreement.NEVER
+ :type interval: str
+
+ :returns: None
+ :raises: ValueError - if the interval is illegal
+ """
+
+ c = re.compile(re.compile('^([0-9][0-9])([0-9][0-9])-([0-9][0-9])' +
+ '([0-9][0-9]) ([0-6]{1,7})$'))
+ if not c.match(interval):
+ raise ValueError("Bad schedule format %r" % interval)
+ schedule = c.split(interval, c.groups)
+
+ # check the hours
+ hour = int(schedule[1])
+ if ((hour < 0) or (hour > 23)):
+ raise ValueError("Bad schedule format %r: illegal hour %d" %
+ (interval, hour))
+ hour = int(schedule[3])
+ if ((hour < 0) or (hour > 23)):
+ raise ValueError("Bad schedule format %r: illegal hour %d" %
+ (interval, hour))
+ if int(schedule[1]) > int(schedule[3]):
+ raise ValueError("Bad schedule (start HOUR larger than end HOUR)"
+
+ " %r: illegal hour %d" %
+ (interval, int(schedule[1])))
+
+ # check the minutes
+ minute = int(schedule[2])
+ if ((minute < 0) or (minute > 59)):
+ raise ValueError("Bad schedule format %r: illegal minute %d" %
+ (interval, minute))
+ minute = int(schedule[4])
+ if ((minute < 0) or (minute > 59)):
+ raise ValueError("Bad schedule format %r: illegal minute %d" %
+ (interval, minute))
+
+ def schedule(self, agmtdn=None, interval=ALWAYS):
+ """Schedule the replication agreement
+
+ :param agmtdn: DN of the replica agreement
+ :type agmtdn: str
+ :param interval: - 'HHMM-HHMM D+' With D=[0123456]+
+ - Agreement.ALWAYS
+ - Agreement.NEVER
+ :type interval: str
+
+ :returns: None
+ :raises: - ValueError - if interval is not valid;
+ - ldap.NO_SUCH_OBJECT - if agmtdn does not exist
+ """
+ if not agmtdn:
+ raise InvalidArgumentError("agreement DN is missing")
+
+ # check the validity of the interval
+ if interval != Agreement.ALWAYS and interval != Agreement.NEVER:
+ self._check_interval(interval)
+
+ # Check if the replica agreement exists
+ try:
+ self.conn.getEntry(agmtdn, ldap.SCOPE_BASE)
+ except ldap.NO_SUCH_OBJECT:
+ raise
+
+ # update it
+ self.log.info("Schedule replication agreement %s" % agmtdn)
+ mod = [(ldap.MOD_REPLACE, 'nsds5replicaupdateschedule',
[ensure_bytes(interval)])]
+ self.conn.modify_s(agmtdn, mod)
+
+ def getProperties(self, agmnt_dn=None, properties=None):
+ """Get a dictionary of the requested properties.
+ If properties parameter is missing, it returns all the properties.
+
+ :param agmtdn: DN of the replica agreement
+ :type agmtdn: str
+ :param properties: List of properties name
+ :type properties: list
+
+ :returns: Returns a dictionary of the properties
+ :raises: - ValueError - if invalid property name
+ - ldap.NO_SUCH_OBJECT - if agmtdn does not exist
+ - InvalidArgumentError - missing mandatory argument
+
+ :supported properties are:
+ RA_NAME, RA_SUFFIX, RA_BINDDN, RA_BINDPW, RA_METHOD,
+ RA_DESCRIPTION, RA_SCHEDULE, RA_TRANSPORT_PROT, RA_FRAC_EXCLUDE,
+ RA_FRAC_EXCLUDE_TOTAL_UPDATE, RA_FRAC_STRIP, RA_CONSUMER_PORT,
+ RA_CONSUMER_HOST, RA_CONSUMER_TOTAL_INIT, RA_TIMEOUT, RA_CHANGES
+ """
+
+ if not agmnt_dn:
+ raise InvalidArgumentError("agmtdn is a mandatory argument")
+
+ #
+ # prepare the attribute list to retrieve from the RA
+ # if properties is None, all RA attributes are retrieved
+ #
+ attrs = []
+ if properties:
+ for prop_name in properties:
+ prop_attr = RA_PROPNAME_TO_ATTRNAME[prop_name]
+ if not prop_attr:
+ raise ValueError("Improper property name: %s ", prop_name)
+ attrs.append(prop_attr)
+
+ filt = "(objectclass=*)"
+ result = {}
+ try:
+ entry = self.conn.getEntry(agmnt_dn, ldap.SCOPE_BASE, filt, attrs)
+
+ # Build the result from the returned attributes
+ for attr in entry.getAttrs():
+ # given an attribute name retrieve the property name
+ props = [k for k, v in six.iteritems(RA_PROPNAME_TO_ATTRNAME)
+ if v.lower() == attr.lower()]
+
+ # If this attribute is present in the RA properties, adds it to
+ # result
+ if len(props) > 0:
+ result[props[0]] = entry.getValues(attr)
+
+ except ldap.NO_SUCH_OBJECT:
+ raise
+
+ return result
+
+ def setProperties(self, suffix=None, agmnt_dn=None, agmnt_entry=None,
+ properties=None):
+ """Set the properties of the agreement entry. If an
'agmnt_entry'
+ is provided, it updates the entry, else it updates the entry on
+ the server. If the 'agmnt_dn' is provided it retrieves the entry
+ using it, else it retrieve the agreement using the 'suffix'.
+
+ :param suffix: Suffix stored in that agreement (online update)
+ :type suffix: str
+ :param agmnt_dn: DN of the agreement (online update)
+ :type agmnt_dn: str
+ :param agmnt_entry: Entry of a agreement (offline update)
+ :type agmnt_entry: lib389.Entry
+ :param properties: Dictionary of properties
+ :type properties: dict
+
+ :returns: None
+ :raises: - ValueError - if invalid properties
+ - ValueError - if invalid agreement_entry
+ - ValueError - if agmnt_dn or suffix are not associated to a replica
+ - InvalidArgumentError - missing mandatory argument
+
+ :supported properties are:
+ RA_NAME, RA_SUFFIX, RA_BINDDN, RA_BINDPW, RA_METHOD,
+ RA_DESCRIPTION, RA_SCHEDULE, RA_TRANSPORT_PROT, RA_FRAC_EXCLUDE,
+ RA_FRAC_EXCLUDE_TOTAL_UPDATE, RA_FRAC_STRIP, RA_CONSUMER_PORT,
+ RA_CONSUMER_HOST, RA_CONSUMER_TOTAL_INIT, RA_TIMEOUT, RA_CHANGES
+ """
+
+ # No properties provided
+ if len(properties) == 0:
+ return
+
+ # check that the given properties are valid
+ for prop in properties:
+ # skip the prefix to add/del value
+ if not inProperties(prop, RA_PROPNAME_TO_ATTRNAME):
+ raise ValueError("unknown property: %s" % prop)
+ else:
+ self.log.debug("setProperties: %s:%s" %
+ (prop, properties[prop]))
+
+ # At least we need to have suffix/agmnt_dn/agmnt_entry
+ if not suffix and not agmnt_dn and not agmnt_entry:
+ raise InvalidArgumentError("suffix and agmnt_dn and agmnt_entry "
+
+ "are missing")
+
+ # TODO
+ if suffix:
+ raise NotImplemented
+
+ # The caller provides a set of properties to set into a replica entry
+ if agmnt_entry:
+ if not isinstance(agmnt_entry, Entry):
+ raise ValueError("invalid instance of the agmnt_entry")
+
+ # that is fine, now set the values
+ for prop in properties:
+ val = rawProperty(prop)
+
+ # for Entry update it is a replace
+ agmnt_entry.update({RA_PROPNAME_TO_ATTRNAME[val]:
+ properties[prop]})
+
+ return
+
+ # for each provided property build the mod
+ mod = []
+ for prop in properties:
+
+ # retrieve/check the property name
+ # and if the value needs to be added/deleted/replaced
+ if prop.startswith('+'):
+ mod_type = ldap.MOD_ADD
+ prop_name = prop[1:]
+ elif prop.startswith('-'):
+ mod_type = ldap.MOD_DELETE
+ prop_name = prop[1:]
+ else:
+ mod_type = ldap.MOD_REPLACE
+ prop_name = prop
+
+ attr = RA_PROPNAME_TO_ATTRNAME[prop_name]
+ if not attr:
+ raise ValueError("invalid property name %s" % prop_name)
+
+ # Now do the value checking we can do
+ if prop_name == RA_SCHEDULE:
+ self._check_interval(properties[prop])
+
+ mod.append((mod_type, attr, ensure_bytes(properties[prop])))
+
+ # Now time to run the effective modify
+ self.conn.modify_s(agmnt_dn, mod)
+
+ def list(self, suffix=None, consumer_host=None, consumer_port=None,
+ agmtdn=None):
+ """Returns the search result of the replica agreement(s) under
the
+ replica (replicaRoot is 'suffix').
+
+ Either 'suffix' or 'agmtdn' need to be specfied.
+ 'consumer_host' and 'consumer_port' are either not specified or
+ specified both.
+
+ If 'agmtdn' is specified, it returns the search result entry of
+ that replication agreement, else if consumer host/port are specified
+ it returns the replica agreements toward that consumer host:port.
+
+ Finally if neither 'agmtdn' nor 'consumser host/port' are
+ specifies it returns all the replica agreements under the replica
+ (replicaRoot is 'suffix').
+
+ :param suffix: The suffix targeted by the total update
+ :type suffix: str
+ :param consumer_host: Hostname of the consumer
+ :type consumer_host: str
+ :param consumer_port: Port of the consumer
+ :type consumer_port: int
+ :param agmtdn: DN of the replica agreement
+ :type agmtdn: str
+
+ :returns: Search result of the replica agreements
+ :raises: - InvalidArgument - if missing mandatory argument
+ (agmtdn or suffix, then host and port)
+ - ValueError - if some properties are not valid
+ - NoSuchEntryError - If no replica defined for the suffix
+ """
+
+ if not suffix and not agmtdn:
+ raise InvalidArgumentError("suffix or agmtdn are required")
+
+ if (consumer_host and not consumer_port) or (not consumer_host and
+ consumer_port):
+ raise InvalidArgumentError(
+ "consumer host/port are required together")
+
+ if agmtdn:
+ # easy case, just return the RA
+ filt = "objectclass=*"
+ return self.conn.search_s(agmtdn, ldap.SCOPE_BASE, filt)
+ else:
+ # Retrieve the replica
+ replica_entries = self.conn.replica.list(suffix)
+ if not replica_entries:
+ raise NoSuchEntryError("Error: no replica set up for suffix "
+ "(%s)" % suffix)
+ replica_entry = replica_entries[0]
+
+ # Now returns the replica agreement for that suffix that replicates
+ # to consumer host/port
+ if consumer_host and consumer_port:
+ '''
+ Currently python does not like long continuous lines when it
+ comes to string formatting, so we need to separate each line
+ like this.
+ '''
+ filt = "(&(|(objectclass=%s)" % RA_OBJECTCLASS_VALUE
+ filt += "(objectclass=%s))" % RA_WINDOWS_OBJECTCLASS_VALUE
+ filt += "(%s=%s)" %
(RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_HOST],
+ consumer_host)
+ filt += "(%s=%d))" % (
+ RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_PORT], consumer_port)
+ else:
+ filt = ("(|(objectclass=%s)(objectclass=%s))" %
+ (RA_OBJECTCLASS_VALUE, RA_WINDOWS_OBJECTCLASS_VALUE))
+
+ return self.conn.search_s(replica_entry.dn, ldap.SCOPE_ONELEVEL,
+ filt)
+
+ def create(self, suffix=None, host=None, port=None, properties=None,
+ winsync=False):
+ """Create (and return) a replication agreement from self to
consumer.
+ Self is the supplier.
+
+ :param suffix: Replication Root
+ :type suffix: str
+ :param host: Consumer host
+ :type host: str
+ :param port: Consumer port
+ :type port: int
+ :param winsync: Identifies the agree as a WinSync agreement
+ :type winsync: bool
+ :param properties: Agreement properties
+ :type properties: dict
+
+ :returns: DN of the created agreement
+ :raises: - InvalidArgumentError - If the suffix is missing
+ - NoSuchEntryError - if a replica doesn't exist for that suffix
+ - ldap.LDAPError - ldap error
+ """
+
+ # Check we have a suffix [ mandatory ]
+ if not suffix:
+ self.log.warning("create: suffix is missing")
+ raise InvalidArgumentError('suffix is mandatory')
+
+ if not properties:
+ properties = {}
+
+ # Compute the normalized suffix to be set in RA entry
+ properties[RA_SUFFIX] = normalizeDN(suffix)
+
+ # Adding agreement under the replica entry
+ replica_entries = self.conn.replica.list(suffix)
+ if not replica_entries:
+ raise NoSuchEntryError(
+ "Error: no replica set up for suffix: %s" % suffix)
+ replica = replica_entries[0]
+
+ # Define agreement entry
+ if RA_NAME not in properties:
+ properties[RA_NAME] = 'meTo_%s:%s' % (host, port)
+ dn_agreement = ','.join(["cn=%s" % properties[RA_NAME],
replica.dn])
+
+ # Set the required properties(if not already set)
+ if RA_BINDDN not in properties:
+ properties[RA_BINDDN] = defaultProperties[REPLICATION_BIND_DN]
+ if RA_BINDPW not in properties:
+ properties[RA_BINDPW] = defaultProperties[REPLICATION_BIND_PW]
+ if RA_METHOD not in properties:
+ properties[RA_METHOD] = defaultProperties[REPLICATION_BIND_METHOD]
+ if RA_TRANSPORT_PROT not in properties:
+ properties[RA_TRANSPORT_PROT] = \
+ defaultProperties[REPLICATION_TRANSPORT]
+ if RA_TIMEOUT not in properties:
+ properties[RA_TIMEOUT] = defaultProperties[REPLICATION_TIMEOUT]
+ if RA_DESCRIPTION not in properties:
+ properties[RA_DESCRIPTION] = properties[RA_NAME]
+ if RA_CONSUMER_HOST not in properties:
+ properties[RA_CONSUMER_HOST] = host
+ if RA_CONSUMER_PORT not in properties:
+ properties[RA_CONSUMER_PORT] = str(port)
+
+ # This is probably unnecessary because
+ # we can just raise ALREADY_EXISTS
+ try:
+ entry = self.conn.getEntry(dn_agreement, ldap.SCOPE_BASE)
+ self.log.warn("Agreement already exists: %r" % dn_agreement)
+ return dn_agreement
+ except ldap.NO_SUCH_OBJECT:
+ entry = None
+
+ # Iterate over the properties, adding them to the entry
+ entry = Entry(dn_agreement)
+ entry.update({'objectclass': ["top", RA_OBJECTCLASS_VALUE]})
+ for prop in properties:
+ entry.update({RA_PROPNAME_TO_ATTRNAME[prop]: properties[prop]})
+
+ # we make a copy here because we cannot change
+ # the passed in properties dict
+ propertiescopy = {}
+ if properties:
+ import copy
+ propertiescopy = copy.deepcopy(properties)
+
+ # Check if this a Winsync Agreement
+ if winsync:
+ self.conn.setupWinSyncAgmt(propertiescopy, entry)
+
+ try:
+ self.log.debug("Adding replica agreement: [%r]" % entry)
+ self.conn.add_s(entry)
+ except ldap.LDAPError as e:
+ self.log.fatal('Failed to add replication agreement: %s' % str(e))
+ raise e
+
+ entry = self.conn.waitForEntry(dn_agreement)
+ if entry:
+ # More verbose but shows what's going on
+ if 'chain' in propertiescopy:
+ raise NotImplementedError
+ chain_args = {
+ 'suffix': suffix,
+ 'binddn': binddn,
+ 'bindpw': bindpw
+ }
+ # Work on `self` aka producer
+ if replica.nsds5replicatype == MASTER_TYPE:
+ self.conn.setupChainingFarm(**chain_args)
+ # Work on `consumer`
+ # TODO - is it really required?
+ if replica.nsds5replicatype == LEAF_TYPE:
+ chain_args.update({
+ 'isIntermediate': 0,
+ 'urls': self.conn.toLDAPURL(),
+ 'args': propertiescopy['chainargs']
+ })
+ consumer.setupConsumerChainOnUpdate(**chain_args)
+ elif replica.nsds5replicatype == HUB_TYPE:
+ chain_args.update({
+ 'isIntermediate': 1,
+ 'urls': self.conn.toLDAPURL(),
+ 'args': propertiescopy['chainargs']
+ })
+ consumer.setupConsumerChainOnUpdate(**chain_args)
+
+ return dn_agreement
+
+ def delete(self, suffix=None, consumer_host=None, consumer_port=None,
+ agmtdn=None):
+ """Delete a replication agreement
+
+ :param suffix: The suffix that the agreement is configured for
+ :type suffix: str
+ :param consumer_host: Host of the server that the agreement points to
+ :type consumer_host: str
+ :param consumer_port: Port of the server that the agreement points to
+ :type consumer_port: int
+ :param agmtdn: DN of the replica agreement
+ :type agmtdn: str
+
+ :returns: None
+ :raises: - ldap.LDAPError - for ldap operation failures
+ - TypeError - if too many agreements were found
+ - NoSuchEntryError - if no agreements were found
+ """
+
+ if not (suffix and consumer_host and consumer_port) and not agmtdn:
+ raise InvalidArgumentError("Suffix with consumer_host and
consumer_port" +
+ " or agmtdn are required")
+
+ agmts = self.list(suffix, consumer_host, consumer_port, agmtdn)
+
+ if agmts:
+ if len(agmts) > 1:
+ raise TypeError('Too many agreements were found')
+ else:
+ # Delete the agreement
+ try:
+ agmt_dn = agmts[0].dn
+ self.conn.delete_s(agmt_dn)
+ self.log.info('Agreement (%s) was successfully removed' %
agmt_dn)
+ except ldap.LDAPError as e:
+ self.log.error('Failed to delete agreement (%s), ' +
+ 'error: %s' % (agmt_dn, str(e)))
+ raise
+ else:
+ raise NoSuchEntryError('No agreements were found')
+
+ def init(self, suffix=None, consumer_host=None, consumer_port=None):
+ """Trigger a total update of the consumer replica
+ - self is the supplier,
+ - consumer is a DirSrv object (consumer can be a master)
+ - cn_format - use this string to format the agreement name
+
+ :param suffix: The suffix targeted by the total update [mandatory]
+ :type suffix: str
+ :param consumer_host: Hostname of the consumer [mandatory]
+ :type consumer_host: str
+ :param consumer_port: Port of the consumer [mandatory]
+ :type consumer_port: int
+
+ :returns: None
+ :raises: InvalidArgument - if missing mandatory argument
+ """
+
+ #
+ # check the required parameters are set
+ #
+ if not suffix:
+ self.log.fatal("initAgreement: suffix is missing")
+ raise InvalidArgumentError('suffix is mandatory argument')
+
+ nsuffix = normalizeDN(suffix)
+
+ if not consumer_host:
+ self.log.fatal("initAgreement: host is missing")
+ raise InvalidArgumentError('host is mandatory argument')
+
+ if not consumer_port:
+ self.log.fatal("initAgreement: port is missing")
+ raise InvalidArgumentError('port is mandatory argument')
+ #
+ # check the replica agreement already exist
+ #
+ replica_entries = self.conn.replica.list(suffix)
+ if not replica_entries:
+ raise NoSuchEntryError(
+ "Error: no replica set up for suffix " + suffix)
+ replica_entry = replica_entries[0]
+ self.log.debug("initAgreement: looking for replica agreements " +
+ "under %s" % replica_entry.dn)
+ try:
+ '''
+ Currently python does not like long continuous lines when it
+ comes to string formatting, so we need to separate each line
+ like this.
+ '''
+ filt = "(&(objectclass=nsds5replicationagreement)"
+ filt += "(nsds5replicahost=%s)" % consumer_host
+ filt += "(nsds5replicaport=%d)" % consumer_port
+ filt += "(nsds5replicaroot=%s))" % nsuffix
+
+ entry = self.conn.getEntry(replica_entry.dn, ldap.SCOPE_ONELEVEL,
+ filt)
+ except ldap.NO_SUCH_OBJECT:
+ msg = ('initAgreement: No replica agreement to ' +
+ '{host}:{port} for suffix {suffix}'.format(
+ host=consumer_host, port=consumer_port, suffix=nsuffix))
+ self.log.fatal(msg)
+ raise
+
+ #
+ # trigger the total init
+ #
+ self.log.info("Starting total init %s" % entry.dn)
+ mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh',
ensure_bytes('start'))]
+ self.conn.modify_s(entry.dn, mod)
+
+ def pause(self, agmtdn, interval=NEVER):
+ """Pause this replication agreement. This replication agreement
+ will send no more changes. Use the resume() method to "unpause".
+
+ It tries to disable the replica agreement. If it fails (not
+ implemented in all version),
+
+ It uses the schedule() with interval '2358-2359 0'
+
+ :param agmtdn: agreement dn
+ :type agmtdn: str
+ :param interval: - 'HHMM-HHMM D+' With D=[0123456]+
+ - Agreement.ALWAYS
+ - Agreement.NEVER
+ - Default is NEVER
+ :type interval: str
+
+ :returns: None
+ :raises: ValueError - if interval is not valid
+ """
+
+ self.log.info("Pausing replication %s" % agmtdn)
+ mod = [(
+ ldap.MOD_REPLACE, 'nsds5ReplicaEnabled', ['off'])]
+ try:
+ self.conn.modify_s(agmtdn, mod)
+ except ldap.LDAPError:
+ # before 1.2.11, no support for nsds5ReplicaEnabled
+ # use schedule hack
+ self.schedule(interval)
+
+ # Allow a little time for repl agmt thread to stop
+ time.sleep(5)
+
+ def resume(self, agmtdn, interval=ALWAYS):
+ """Resume a paused replication agreement, paused with the
"pause" method.
+ It tries to enabled the replica agreement. If it fails
+ (not implemented in all versions)
+ It uses the schedule() with interval '0000-2359 0123456'.
+
+ :param agmtdn: agreement dn
+ :type agmtdn: str
+ :param interval: - 'HHMM-HHMM D+' With D=[0123456]+
+ - Agreement.ALWAYS
+ - Agreement.NEVER
+ - Default is NEVER
+ :type interval: str
+
+ :returns: None
+ :raises: - ValueError - if interval is not valid
+ - ldap.NO_SUCH_OBJECT - if agmtdn does not exist
+ """
+
+ self.log.info("Resuming replication %s" % agmtdn)
+ mod = [(
+ ldap.MOD_REPLACE, 'nsds5ReplicaEnabled', ['on'])]
+ try:
+ self.conn.modify_s(agmtdn, mod)
+ except ldap.LDAPError:
+ # before 1.2.11, no support for nsds5ReplicaEnabled
+ # use schedule hack
+ self.schedule(interval)
+
+ # Allow a little time for the repl agmt thread to start
+ time.sleep(2)
+
+ def changes(self, agmnt_dn):
+ """Get a number of changes sent by this agreement.
+
+ :param agmtdn: agreement dn
+ :type agmtdn: str
+
+ :returns: Number of changes
+ :raises: NoSuchEntryError - if agreement entry with changes
+ attribute is not found
+ """
+
+ retval = 0
+ try:
+ ent = self.conn.getEntry(ensure_str(agmnt_dn), ldap.SCOPE_BASE,
+ "(objectclass=*)",
+ [RA_PROPNAME_TO_ATTRNAME[RA_CHANGES]])
+ except:
+ raise NoSuchEntryError(
+ "Error reading status from agreement", agmnt_dn)
+
+ if ent.nsds5replicaChangesSentSinceStartup:
+ val = ent.nsds5replicaChangesSentSinceStartup
+ items = val.split(ensure_bytes(' '))
+ if len(items) == 1:
+ retval = int(items[0])
+ else:
+ for item in items:
+ ary = item.split(ensure_bytes(":"))
+ if ary and len(ary) > 1:
+ retval = retval +
int(ary[1].split(ensure_bytes("/"))[0])
+ return retval
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
new file mode 100644
index 0000000..6f4c869
--- /dev/null
+++ b/src/lib389/lib389/backend.py
@@ -0,0 +1,576 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+from lib389._constants import *
+from lib389.properties import *
+from lib389.utils import normalizeDN, ensure_str, ensure_bytes
+from lib389 import Entry
+
+# Need to fix this ....
+
+from lib389._mapped_object import DSLdapObjects, DSLdapObject
+from lib389.mappingTree import MappingTrees, MappingTree
+from lib389.exceptions import NoSuchEntryError, InvalidArgumentError
+
+# We need to be a factor to the backend monitor
+from lib389.monitor import MonitorBackend
+from lib389.index import Indexes
+
+# This is for sample entry creation.
+from lib389.configurations import get_sample_entries
+
+from lib389.lint import DSBLE0001
+
+class BackendLegacy(object):
+ proxied_methods = 'search_s getEntry'.split()
+
+ def __init__(self, conn):
+ """@param conn - a DirSrv instance"""
+ self.conn = conn
+ self.log = conn.log
+
+ def __getattr__(self, name):
+ if name in Backend.proxied_methods:
+ from lib389 import DirSrv
+ return DirSrv.__getattr__(self.conn, name)
+
+ def list(self, suffix=None, backend_dn=None, bename=None):
+ """
+ Returns a search result of the backend(s) entries with all their
+ attributes
+
+ If 'suffix'/'backend_dn'/'benamebase' are specified.
It uses
+ 'backend_dn' first, then 'suffix', then
'benamebase'.
+
+ If neither 'suffix', 'backend_dn' and 'benamebase'
are specified,
+ it returns all the backend entries
+
+ Get backends by name or suffix
+
+ @param suffix - suffix of the backend
+ @param backend_dn - DN of the backend entry
+ @param bename - 'commonname'/'cn' of the backend (e.g.
'userRoot')
+
+ @return backend entries
+
+ @raise None
+ """
+
+ filt = "(objectclass=%s)" % BACKEND_OBJECTCLASS_VALUE
+ if backend_dn:
+ self.log.info("List backend %s" % backend_dn)
+ base = backend_dn
+ scope = ldap.SCOPE_BASE
+ elif suffix:
+ self.log.info("List backend with suffix=%s" % suffix)
+ base = DN_PLUGIN
+ scope = ldap.SCOPE_SUBTREE
+ filt = ("(&%s(|(%s=%s)(%s=%s)))" %
+ (filt,
+ BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_SUFFIX], suffix,
+ BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_SUFFIX],
+ normalizeDN(suffix))
+ )
+ elif bename:
+ self.log.info("List backend 'cn=%s'" % bename)
+ base = "%s=%s,%s" % (BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_NAME],
+ bename, DN_LDBM)
+ scope = ldap.SCOPE_BASE
+ else:
+ self.log.info("List all the backends")
+ base = DN_PLUGIN
+ scope = ldap.SCOPE_SUBTREE
+
+ try:
+ ents = self.conn.search_s(base, scope, filt)
+ except ldap.NO_SUCH_OBJECT:
+ return None
+
+ return ents
+
+ def _readonly(self, bename=None, readonly='on', suffix=None):
+ """Put a database in readonly mode
+ @param bename - the backend name (eg. addressbook1)
+ @param readonly- 'on' or 'off'
+
+ NOTE: I can ldif2db to a read-only database. After the
+ import, the database will still be in readonly.
+
+ NOTE: When a db is read-only, it seems you need to restart
+ the directory server before creating further
+ agreements or initialize consumers
+ """
+ if bename and suffix:
+ raise ValueError("Specify either bename or suffix")
+
+ if suffix:
+ raise NotImplementedError()
+
+ self.conn.modify_s(','.join(('cn=' + bename, DN_LDBM)), [
+ (ldap.MOD_REPLACE, BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_READONLY],
+ readonly)
+ ])
+
+ def delete(self, suffix=None, backend_dn=None, bename=None):
+ """
+ Deletes the backend entry with the following steps:
+
+ Delete the indexes entries under this backend
+ Delete the encrypted attributes entries under this backend
+ Delete the encrypted attributes keys entries under this backend
+
+ If a mapping tree entry uses this backend (nsslapd-backend),
+ it raise ldap.UNWILLING_TO_PERFORM
+
+ If 'suffix'/'backend_dn'/'benamebase' are specified.
+ It uses 'backend_dn' first, then 'suffix', then
'benamebase'.
+
+ @param suffix - suffix of the backend
+ @param backend_dn - DN of the backend entry
+ @param bename - 'commonname'/'cn' of the backend (e.g.
'userRoot')
+
+ @return None
+
+ @raise ldap.UNWILLING_TO_PERFORM - if several backends match the
+ argument provided suffix does not
+ match backend suffix. It exists a
+ mapping tree that use that backend
+
+
+ """
+
+ # First check the backend exists and retrieved its suffix
+ be_ents = self.conn.backend.list(suffix=suffix,
+ backend_dn=backend_dn,
+ bename=bename)
+ if len(be_ents) == 0:
+ raise ldap.UNWILLING_TO_PERFORM(
+ "Unable to retrieve the backend (%r, %r, %r)" %
+ (suffix, backend_dn, bename))
+ elif len(be_ents) > 1:
+ for ent in be_ents:
+ self.log.fatal("Multiple backend match the definition: %s" %
+ ent.dn)
+ if (not suffix) and (not backend_dn) and (not bename):
+ raise ldap.UNWILLING_TO_PERFORM(
+ "suffix and backend DN and backend name are missing")
+ raise ldap.UNWILLING_TO_PERFORM(
+ "Not able to identify the backend to delete")
+ else:
+ be_ent = be_ents[0]
+ be_suffix = be_ent.getValue(
+ BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_SUFFIX])
+
+ # Verify the provided suffix is the one stored in the found backend
+ if suffix:
+ if normalizeDN(suffix) != normalizeDN(be_suffix):
+ raise ldap.UNWILLING_TO_PERFORM(
+ "provided suffix (%s) differs from backend suffix (%s)"
+ % (suffix, be_suffix))
+
+ # now check there is no mapping tree for that suffix
+ mt_ents = self.conn.mappingtree.list(suffix=be_suffix)
+ if len(mt_ents) > 0:
+ raise ldap.UNWILLING_TO_PERFORM(
+ "It still exists a mapping tree (%s) for that backend (%s)" %
+ (mt_ents[0].dn, be_ent.dn))
+
+ # Now delete the indexes
+ found_bename =
ensure_str(be_ent.getValue(BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_NAME]))
+ if not bename:
+ bename = found_bename
+ elif bename.lower() != found_bename.lower():
+ raise ldap.UNWILLING_TO_PERFORM("Backend name specified (%s) differs
from the retrieved one (%s)" % (bename, found_bename))
+
+ self.conn.index.delete_all(bename)
+
+ # finally delete the backend children and the backend itself
+ ents = self.conn.search_s(be_ent.dn, ldap.SCOPE_ONELEVEL)
+ for ent in ents:
+ self.log.debug("Delete entry children %s" % (ent.dn))
+ self.conn.delete_s(ent.dn)
+
+ self.log.debug("Delete backend entry %s" % (be_ent.dn))
+ self.conn.delete_s(be_ent.dn)
+
+ return
+
+ def create(self, suffix=None, properties=None):
+ """
+ Creates backend entry and returns its dn.
+
+ If the properties 'chain-bind-pwd' and 'chain-bind-dn' and
+ 'chain-urls' are specified the backend is a chained backend. A
+ chaining backend is created under
+ 'cn=chaining database,cn=plugins,cn=config'.
+
+ A local backend is created under
+ 'cn=ldbm database,cn=plugins,cn=config'
+
+ @param suffix - suffix stored in the backend
+ @param properties - dictionary with properties values
+ supported properties are
+ BACKEND_NAME = 'name'
+ BACKEND_READONLY = 'read-only'
+ BACKEND_REQ_INDEX = 'require-index'
+ BACKEND_CACHE_ENTRIES = 'entry-cache-number'
+ BACKEND_CACHE_SIZE = 'entry-cache-size'
+ BACKEND_DNCACHE_SIZE = 'dn-cache-size'
+ BACKEND_DIRECTORY = 'directory'
+ BACKEND_DB_DEADLOCK = 'db-deadlock'
+ BACKEND_CHAIN_BIND_DN = 'chain-bind-dn'
+ BACKEND_CHAIN_BIND_PW = 'chain-bind-pw'
+ BACKEND_CHAIN_URLS = 'chain-urls'
+ BACKEND_SUFFIX = 'suffix'
+ BACKEND_SAMPLE_ENTRIES = 'sample_entries'
+
+ @return backend DN of the created backend
+
+ @raise LDAPError
+
+ """
+ def _getBackendName(parent):
+ '''
+ Use to build a backend name that is not already used
+ '''
+ index = 1
+ while True:
+ bename = "local%ddb" % index
+ base = ("%s=%s,%s" %
+ (BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_NAME],
+ bename, parent))
+ filt = "(objectclass=%s)" % BACKEND_OBJECTCLASS_VALUE
+ self.log.debug("_getBackendName: baser=%s : fileter=%s" %
+ (base, filt))
+ try:
+ self.conn.getEntry(base, ldap.SCOPE_BASE, filt)
+ except (NoSuchEntryError, ldap.NO_SUCH_OBJECT):
+ self.log.info("backend name will be %s" % bename)
+ return bename
+ index += 1
+
+ # suffix is mandatory. If may be in the properties
+ if isinstance(properties, dict) and properties.get(BACKEND_SUFFIX, None) is not
None:
+ suffix = properties.get(BACKEND_SUFFIX)
+ if not suffix:
+ raise ldap.UNWILLING_TO_PERFORM('Missing Suffix')
+ else:
+ nsuffix = normalizeDN(suffix)
+
+ # Check it does not already exist a backend for that suffix
+ if self.conn.verbose:
+ self.log.info("Checking suffix %s for existence" % suffix)
+ ents = self.conn.backend.list(suffix=suffix)
+ if len(ents) != 0:
+ raise ldap.ALREADY_EXISTS
+ # Check if we are creating a local/chained backend
+ chained_suffix = (properties and
+ (BACKEND_CHAIN_BIND_DN in properties) and
+ (BACKEND_CHAIN_BIND_PW in properties) and
+ (BACKEND_CHAIN_URLS in properties))
+ if chained_suffix:
+ self.log.info("Creating a chaining backend")
+ dnbase = DN_CHAIN
+ else:
+ self.log.info("Creating a local backend")
+ dnbase = DN_LDBM
+
+ # Get the future backend name
+ if properties and BACKEND_NAME in properties:
+ cn = properties[BACKEND_NAME]
+ else:
+ cn = _getBackendName(dnbase)
+
+ # Check the future backend name does not already exists
+ # we can imagine having no backends for 'suffix' but having a backend
+ # with the same name
+ dn = "%s=%s,%s" % (BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_NAME], cn,
+ dnbase)
+ ents = self.conn.backend.list(backend_dn=dn)
+ if ents:
+ raise ldap.ALREADY_EXISTS("Backend already exists with that DN:
%s"
+ % ents[0].dn)
+
+ # All checks are done, Time to create the backend
+ try:
+ entry = Entry(dn)
+ entry.update({
+ 'objectclass': ['top', 'extensibleObject',
+ BACKEND_OBJECTCLASS_VALUE],
+ BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_NAME]: cn,
+ BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_SUFFIX]: nsuffix,
+ })
+
+ if chained_suffix:
+ entry.update(
+ {BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_CHAIN_URLS]:
+ properties[BACKEND_CHAIN_URLS],
+ BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_CHAIN_BIND_DN]:
+ properties[BACKEND_CHAIN_BIND_DN],
+ BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_CHAIN_BIND_PW]:
+ properties[BACKEND_CHAIN_BIND_PW]
+ })
+
+ self.log.debug("adding entry: %r" % entry)
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS as e:
+ self.log.error("Entry already exists: %r" % dn)
+ raise ldap.ALREADY_EXISTS("%s : %r" % (e, dn))
+ except ldap.LDAPError as e:
+ self.log.error("Could not add backend entry: %r" % dn)
+ raise e
+
+ backend_entry = self.conn._test_entry(dn, ldap.SCOPE_BASE)
+
+ return backend_entry
+
+ def getProperties(self, suffix=None, backend_dn=None, bename=None,
+ properties=None):
+ raise NotImplemented
+
+ def setProperties(self, suffix=None, backend_dn=None, bename=None,
+ properties=None):
+ raise NotImplemented
+
+ def toSuffix(self, entry=None, name=None):
+ '''
+ Return, for a given backend entry, the suffix values.
+ Suffix values are identical from a LDAP point of views.
+ Suffix values may be surrounded by ", or containing '\'
+ escape characters.
+
+ @param entry - LDAP entry of the backend
+ @param name - backend DN
+
+ @result list of values of suffix attribute (aka 'cn')
+
+ @raise ldap.NO_SUCH_OBJECT - in name is invalid DN
+ ValueError - entry does not contains the suffix attribute
+ InvalidArgumentError - if both entry/name are missing
+ '''
+ attr_suffix = BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_SUFFIX]
+ if entry:
+ if not entry.hasValue(attr_suffix):
+ raise ValueError("Entry has no %s attribute %r" %
+ (attr_suffix, entry))
+ return entry.getValues(attr_suffix)
+ elif name:
+ filt = "(objectclass=%s)" % BACKEND_OBJECTCLASS_VALUE
+
+ attrs = [attr_suffix]
+ ent = self.conn.getEntry(name, ldap.SCOPE_BASE, filt, attrs)
+ self.log.debug("toSuffix: %s found by its DN" % ent.dn)
+
+ if not ent.hasValue(attr_suffix):
+ raise ValueError("Entry has no %s attribute %r" %
+ (attr_suffix, ent))
+ return ent.getValues(attr_suffix)
+ else:
+ raise InvalidArgumentError("entry or name are mandatory")
+
+ def requireIndex(self, suffix):
+ '''
+ Should be moved in setProperties
+ '''
+ entries_backend = self.backend.list(suffix=suffix)
+ # assume 1 local backend
+ dn = entries_backend[0].dn
+ replace = [(ldap.MOD_REPLACE, 'nsslapd-require-index', 'on')]
+ self.modify_s(dn, replace)
+
+
+class Backend(DSLdapObject):
+ """Backend DSLdapObject with:
+ - must attributes = ['cn, 'nsslapd-suffix']
+ - RDN attribute is 'cn'
+
+ :param instance: A instance
+ :type instance: lib389.DirSrv
+ :param dn: Entry DN
+ :type dn: str
+ :param batch: Not implemented
+ :type batch: bool
+ """
+
+ _must_attributes = ['nsslapd-suffix', 'cn']
+
+ def __init__(self, instance, dn=None, batch=False):
+ super(Backend, self).__init__(instance, dn, batch)
+ self._rdn_attribute = 'cn'
+ self._must_attributes = ['nsslapd-suffix', 'cn']
+ self._create_objectclasses = ['top', 'extensibleObject',
BACKEND_OBJECTCLASS_VALUE]
+ self._protected = False
+ self._lint_functions = [self._lint_mappingtree]
+ # Check if a mapping tree for this suffix exists.
+ self._mts = MappingTrees(self._instance)
+
+ def create_sample_entries(self, version):
+ """Creates sample entries under nsslapd-suffix value
+
+ :param version: Sample entries version, i.e. 001003006
+ :type version: str
+ """
+
+ self._log.debug('Creating sample entries at version %s....' % version)
+ # Grab the correct sample entry config
+ centries = get_sample_entries(version)
+ # apply it.
+ basedn = self.get_attr_val('nsslapd-suffix')
+ cent = centries(self._instance, basedn)
+ cent.apply()
+
+ def _validate(self, rdn, properties, basedn):
+ # We always need to call the super validate first. This way we can
+ # guarantee that properties is a dictionary.
+ # However, backend can take different properties. One is
+ # based on the actual key, value of the object
+ # one is the "python magic" types.
+ # So we actually have to do the super validation later.
+ if properties is None:
+ raise ldap.UNWILLING_TO_PERFORM('Invalid request to create. Properties
cannot be None')
+ if type(properties) != dict:
+ raise ldap.UNWILLING_TO_PERFORM("properties must be a dictionary")
+
+ # This is converting the BACKEND_ types to the DS nsslapd- attribute values
+ nprops = {}
+ for key, value in properties.items():
+ try:
+ nprops[BACKEND_PROPNAME_TO_ATTRNAME[key]] = [value, ]
+ except KeyError:
+ # This means, it's not a mapped value, so continue
+ nprops[key] = value
+
+ (dn, valid_props) = super(Backend, self)._validate(rdn, nprops, basedn)
+
+ try:
+ self._mts.get(ensure_str(valid_props['nsslapd-suffix'][0]))
+ raise ldap.UNWILLING_TO_PERFORM("Mapping tree for this suffix
exists!")
+ except ldap.NO_SUCH_OBJECT:
+ pass
+ try:
+ self._mts.get(ensure_str(valid_props['cn'][0]))
+ raise ldap.UNWILLING_TO_PERFORM("Mapping tree for this database
exists!")
+ except ldap.NO_SUCH_OBJECT:
+ pass
+ # We have to stash our valid props so that mapping tree can use them ...
+ self._nprops_stash = valid_props
+
+ return (dn, valid_props)
+
+ def create(self, dn=None, properties=None, basedn=None):
+ """Add a new backend entry, create mapping tree,
+ and, if requested, sample entries
+
+ :param rdn: RDN of the new entry
+ :type rdn: str
+ :param properties: Attributes and parameters for the new entry
+ :type properties: dict
+ :param basedn: Base DN of the new entry
+ :type rdn: str
+
+ :returns: DSLdapObject of the created entry
+ """
+
+ sample_entries = properties.pop(BACKEND_SAMPLE_ENTRIES, False)
+ # Okay, now try to make the backend.
+ super(Backend, self).create(dn, properties, basedn)
+ # We check if the mapping tree exists in create, so do this *after*
+ self._mts.create(properties = {
+ 'cn': self._nprops_stash['nsslapd-suffix'],
+ 'nsslapd-state' : 'backend',
+ 'nsslapd-backend' : self._nprops_stash['cn'] ,
+ })
+ if sample_entries is not False:
+ self.create_sample_entries(sample_entries)
+ return self
+
+ def delete(self):
+ """Deletes the backend, it's mapping tree and all related
indices.
+ This can be changed with the self._protected flag!
+
+ :raises: - UnwillingToPerform - if backend is protected
+ - UnwillingToPerform - if nsslapd-state is not 'backend'
+ """
+
+ if self._protected:
+ raise ldap.UNWILLING_TO_PERFORM("This is a protected backend!")
+ # First check if the mapping tree has our suffix still.
+ # suffix = self.get_attr_val('nsslapd-suffix')
+ bename = self.get_attr_val_utf8('cn')
+ try:
+ mt = self._mts.get(selector=bename)
+ # Assert the type is "backend"
+ # Are these the right types....?
+ if mt.get_attr_val('nsslapd-state') !=
ensure_bytes('backend'):
+ raise ldap.UNWILLING_TO_PERFORM('Can not delete the mapping tree, not
for a backend! You may need to delete this backend via cn=config .... ;_; ')
+ # Delete our mapping tree if it exists.
+ mt.delete()
+ except ldap.NO_SUCH_OBJECT:
+ # Righto, it's already gone! Do nothing ...
+ pass
+ # Delete all our related indices
+ self._instance.index.delete_all(bename)
+
+ # Now remove our children, this is all ldbm config
+ self._instance.delete_branch_s(self._dn, ldap.SCOPE_ONELEVEL)
+ # The super will actually delete ourselves.
+ super(Backend, self).delete()
+
+ def _lint_mappingtree(self):
+ """Backend lint
+
+ This should check for:
+ * missing mapping tree entries for the backend
+ * missing indices if we are local and have log access?
+ """
+
+ # Check for the missing mapping tree.
+ suffix = self.get_attr_val_utf8('nsslapd-suffix')
+ bename = self.get_attr_val_bytes('cn')
+ try:
+ mt = self._mts.get(suffix)
+ if mt.get_attr_val_bytes('nsslapd-backend') != bename and
mt.get_attr_val('nsslapd-state') != ensure_bytes('backend') :
+ raise ldap.NO_SUCH_OBJECT("We have a matching suffix, but not a
backend or correct database name.")
+ except ldap.NO_SUCH_OBJECT:
+ result = DSBLE0001
+ result['items'] = [bename, ]
+ return result
+ return None
+
+ def get_monitor(self):
+ """Get a MonitorBackend(DSLdapObject) for the
backend"""
+
+ monitor = MonitorBackend(instance=self._instance, dn= "cn=monitor,%s" %
self._dn, batch=self._batch)
+ return monitor
+
+ def get_indexes(self):
+ """Get an Indexes(DSLdapObject) for the backend"""
+
+ indexes = Indexes(self._instance, basedn="cn=index,%s" % self._dn)
+ return indexes
+ # Future: add reindex task for this be.
+
+
+class Backends(DSLdapObjects):
+ """DSLdapObjects that represents DN_LDBM base DN
+ This only does ldbm backends. Chaining backends are a special case
+ of this, so they can be subclassed off.
+
+ :param instance: A instance
+ :type instance: lib389.DirSrv
+ :param batch: Not implemented
+ :type batch: bool
+ """
+
+ def __init__(self, instance, batch=False):
+ super(Backends, self).__init__(instance=instance, batch=batch)
+ self._objectclasses = [BACKEND_OBJECTCLASS_VALUE]
+ self._filterattrs = ['cn', 'nsslapd-suffix',
'nsslapd-directory']
+ self._childobject = Backend
+ self._basedn = DN_LDBM
diff --git a/src/lib389/lib389/changelog.py b/src/lib389/lib389/changelog.py
new file mode 100644
index 0000000..22fe01a
--- /dev/null
+++ b/src/lib389/lib389/changelog.py
@@ -0,0 +1,153 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import os
+import ldap
+
+from lib389._constants import *
+from lib389.properties import *
+from lib389 import DirSrv, Entry, InvalidArgumentError
+
+
+class Changelog(object):
+ """An object that helps to work with changelog entry
+
+ :param conn: An instance
+ :type conn: lib389.DirSrv
+ """
+
+ proxied_methods = 'search_s getEntry'.split()
+
+ def __init__(self, conn):
+ self.conn = conn
+ self.log = conn.log
+
+ def __getattr__(self, name):
+ if name in Changelog.proxied_methods:
+ return DirSrv.__getattr__(self.conn, name)
+
+ def list(self, suffix=None, changelogdn=DN_CHANGELOG):
+ """Get a changelog entry using changelogdn parameter
+
+ :param suffix: Not used
+ :type suffix: str
+ :param changelogdn: DN of the changelog entry, DN_CHANGELOG by default
+ :type changelogdn: str
+
+ :returns: Search result of the replica agreements.
+ Enpty list if nothing was found
+ """
+
+ base = changelogdn
+ filtr = "(objectclass=extensibleobject)"
+
+ # now do the effective search
+ try:
+ ents = self.conn.search_s(base, ldap.SCOPE_BASE, filtr)
+ except ldap.NO_SUCH_OBJECT:
+ # There are no objects to select from, se we return an empty array
+ # as we do in DSLdapObjects
+ ents = []
+ return ents
+
+ def create(self, dbname=DEFAULT_CHANGELOG_DB):
+ """Add and return the replication changelog entry.
+
+ :param dbname: Database name, it will be used for creating
+ a changelog dir path
+ :type dbname: str
+ """
+
+ dn = DN_CHANGELOG
+ attribute, changelog_name = dn.split(",")[0].split("=", 1)
+ dirpath = os.path.join(os.path.dirname(self.conn.dbdir), dbname)
+ entry = Entry(dn)
+ entry.update({
+ 'objectclass': ("top", "extensibleobject"),
+ CHANGELOG_PROPNAME_TO_ATTRNAME[CHANGELOG_NAME]: changelog_name,
+ CHANGELOG_PROPNAME_TO_ATTRNAME[CHANGELOG_DIR]: dirpath
+ })
+ self.log.debug("adding changelog entry: %r" % entry)
+ self.conn.changelogdir = dirpath
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.warn("entry %s already exists" % dn)
+ return dn
+
+ def delete(self):
+ """Delete the changelog entry
+
+ :raises: LDAPError - failed to delete changelog entry
+ """
+
+ try:
+ self.conn.delete_s(DN_CHANGELOG)
+ except ldap.LDAPError as e:
+ self.log.error('Failed to delete the changelog: ' + str(e))
+ raise
+
+ def setProperties(self, changelogdn=None, properties=None):
+ """Set the properties of the changelog entry.
+
+ :param changelogdn: DN of the changelog
+ :type changelogdn: str
+ :param properties: Dictionary of properties
+ :type properties: dict
+
+ :returns: None
+ :raises: - ValueError - if invalid properties
+ - ValueError - if changelog entry is not found
+ - InvalidArgumentError - changelog DN is missing
+
+ :supported properties are:
+ CHANGELOG_NAME, CHANGELOG_DIR, CHANGELOG_MAXAGE,
+ CHANGELOG_MAXENTRIES, CHANGELOG_TRIM_INTERVAL,
+ CHANGELOG_COMPACT_INTV, CHANGELOG_CONCURRENT_WRITES,
+ CHANGELOG_ENCRYPT_ALG, CHANGELOG_SYM_KEY
+ """
+
+ if not changelogdn:
+ raise InvalidArgumentError("changelog DN is missing")
+
+ ents = self.conn.changelog.list(changelogdn=changelogdn)
+ if len(ents) != 1:
+ raise ValueError("Changelog entry not found: %s" % changelogdn)
+
+ # check that the given properties are valid
+ for prop in properties:
+ # skip the prefix to add/del value
+ if not inProperties(prop, CHANGELOG_PROPNAME_TO_ATTRNAME):
+ raise ValueError("unknown property: %s" % prop)
+
+ # build the MODS
+ mods = []
+ for prop in properties:
+ # take the operation type from the property name
+ val = rawProperty(prop)
+ if str(prop).startswith('+'):
+ op = ldap.MOD_ADD
+ elif str(prop).startswith('-'):
+ op = ldap.MOD_DELETE
+ else:
+ op = ldap.MOD_REPLACE
+
+ mods.append((op, CHANGELOG_PROPNAME_TO_ATTRNAME[val],
+ properties[prop]))
+
+ # that is fine now to apply the MOD
+ self.conn.modify_s(ents[0].dn, mods)
+
+ def getProperties(self, changelogdn=None, properties=None):
+ """Get a dictionary of the requested properties.
+ If properties parameter is missing, it returns all the properties.
+
+ NotImplemented
+ """
+
+ raise NotImplemented
diff --git a/src/lib389/lib389/cli_base/__init__.py
b/src/lib389/lib389/cli_base/__init__.py
new file mode 100644
index 0000000..21bf3cc
--- /dev/null
+++ b/src/lib389/lib389/cli_base/__init__.py
@@ -0,0 +1,170 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import logging
+import sys
+
+from getpass import getpass
+from lib389 import DirSrv
+from lib389.properties import SER_LDAP_URL, SER_ROOT_DN, SER_ROOT_PW
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+# REALLY PYTHON 3? REALLY???
+def _input(msg):
+ if MAJOR >= 3:
+ return input(msg)
+ else:
+ return raw_input(msg)
+
+
+def _get_arg(args, msg=None):
+ if args is not None and len(args) > 0:
+ if type(args) is list:
+ return args[0]
+ else:
+ return args
+ else:
+ return _input("%s : " % msg)
+
+def _get_args(args, kws):
+ kwargs = {}
+ while len(kws) > 0:
+ kw, msg, priv = kws.pop(0)
+
+ if args is not None and len(args) > 0:
+ kwargs[kw] = args.pop(0)
+ else:
+ if priv:
+ kwargs[kw] = getpass("%s : " % msg)
+ else:
+ kwargs[kw] = _input("%s : " % msg)
+ return kwargs
+
+# This is really similar to get_args, but generates from an array
+def _get_attributes(args, attrs):
+ kwargs = {}
+ for attr in attrs:
+ if args is not None and hasattr(args, attr) and getattr(args, attr) is not None:
+ kwargs[attr] = getattr(args, attr)
+ else:
+ if attr.lower() == 'userpassword':
+ kwargs[attr] = getpass("Enter value for %s : " % attr)
+ else:
+ kwargs[attr] = _input("Enter value for %s : " % attr)
+ return kwargs
+
+
+def _warn(data, msg=None):
+ if msg is not None:
+ print("%s :" % msg)
+ if 'Yes I am sure' != _input("Type 'Yes I am sure' to continue:
"):
+ raise Exception("Not sure if want")
+ return data
+
+# We'll need another of these that does a "connect via instance name?"
+def connect_instance(dsrc_inst, verbose):
+ dsargs = {
+ SER_LDAP_URL: dsrc_inst['uri'],
+ SER_ROOT_DN: dsrc_inst['binddn'],
+ }
+ ds = DirSrv(verbose=verbose)
+ ds.allocate(dsargs)
+ if not ds.can_autobind() and dsrc_inst['binddn'] is not None:
+ dsargs[SER_ROOT_PW] = getpass("Enter password for %s on %s : " %
(dsrc_inst['binddn'], dsrc_inst['uri']))
+ elif dsrc_inst['binddn'] is None:
+ raise Exception("Must provide a binddn to connect with")
+ ds.allocate(dsargs)
+ ds.open(saslmethod=dsrc_inst['saslmech'],
+ certdir=dsrc_inst['tls_cacertdir'],
+ reqcert=dsrc_inst['tls_reqcert'],
+ usercert=dsrc_inst['tls_cert'],
+ userkey=dsrc_inst['tls_key'],
+ starttls=dsrc_inst['starttls'], connOnly=True)
+ return ds
+
+def disconnect_instance(inst):
+ if inst is not None:
+ inst.close()
+
+def populate_attr_arguments(parser, attributes):
+ for attr in attributes:
+ parser.add_argument('--%s' % attr, nargs='?', help="Value of
%s" % attr)
+
+def _generic_list(inst, basedn, log, manager_class, **kwargs):
+ mc = manager_class(inst, basedn)
+ ol = mc.list()
+ if len(ol) == 0:
+ log.info("No objects to display")
+ elif len(ol) > 0:
+ # We might sort this in the future
+ for o in ol:
+ o_str = o.__unicode__()
+ log.info(o_str)
+
+# Display these entries better!
+def _generic_get(inst, basedn, log, manager_class, selector):
+ mc = manager_class(inst, basedn)
+ o = mc.get(selector)
+ o_str = o.display()
+ log.info(o_str)
+
+def _generic_get_dn(inst, basedn, log, manager_class, dn):
+ mc = manager_class(inst, basedn)
+ o = mc.get(dn=dn)
+ o_str = o.display()
+ log.info(o_str)
+
+def _generic_create(inst, basedn, log, manager_class, kwargs):
+ mc = manager_class(inst, basedn)
+ o = mc.create(properties=kwargs)
+ o_str = o.__unicode__()
+ log.info('Sucessfully created %s' % o_str)
+
+def _generic_delete(inst, basedn, log, object_class, dn):
+ # Load the oc direct
+ o = object_class(inst, dn)
+ o.delete()
+ log.info('Sucessfully deleted %s' % dn)
+
+
+class LogCapture(logging.Handler):
+ """
+ This useful class is for intercepting logs, and then making assertions about
+ the outputs provided. Used by the cli unit tests.
+ """
+
+ def __init__(self):
+ """
+ Create a log instance and primes the output capture.
+ """
+ super(LogCapture, self).__init__()
+ self.outputs = []
+ self.log = logging.getLogger("LogCapture")
+ self.log.addHandler(self)
+
+ def emit(self, record):
+ self.outputs.append(record)
+
+ def contains(self, query):
+ """
+ Assert that the query string listed is in some logged Record.
+ """
+ result = False
+ for rec in self.outputs:
+ if query in str(rec):
+ result = True
+ return result
+
+ def flush(self):
+ self.outputs = []
+
+class FakeArgs(object):
+ def __init__(self):
+ pass
+
diff --git a/src/lib389/lib389/cli_base/dsrc.py b/src/lib389/lib389/cli_base/dsrc.py
new file mode 100644
index 0000000..4a2d0ac
--- /dev/null
+++ b/src/lib389/lib389/cli_base/dsrc.py
@@ -0,0 +1,118 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import sys
+import os
+import ldap
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+if MAJOR >= 3:
+ import configparser
+
+def dsrc_arg_concat(args, dsrc_inst):
+ """
+ Given a set of argparse args containing:
+
+ instance
+ binddn
+ starttls
+
+ and a dsrc_inst (as from dsrc_to_ldap)
+
+ Using this, overlay the settings together to form an instance
+
+ Generally, if dsrc_inst is None, we make a new instance.
+
+ If dsrc_inst is populated we overlay args as needed on top.
+ """
+ if dsrc_inst is None:
+ new_dsrc_inst = {
+ 'uri': args.instance,
+ 'basedn': args.basedn,
+ 'binddn': args.binddn,
+ 'saslmech': None,
+ 'tls_cacertdir': None,
+ 'tls_cert': None,
+ 'tls_key': None,
+ 'tls_reqcert': ldap.OPT_X_TLS_HARD,
+ 'starttls': args.starttls,
+ }
+ # Make new
+ return new_dsrc_inst
+ # overlay things.
+ if args.basedn is not None:
+ dsrc_inst['basedn'] = args.basedn
+ if args.binddn is not None:
+ dsrc_inst['binddn'] = args.binddn
+ if args.starttls is True:
+ dsrc_inst['starttls'] = True
+ return dsrc_inst
+
+def dsrc_to_ldap(path, instance_name, log):
+ """
+ Given a path to a file, return the required details for an instance.
+
+ The file should be an ini file, and instance should identify a section.
+
+ The ini fileshould have the content:
+
+ [instance]
+ uri = ldaps://hostname:port
+ basedn = dc=example,dc=com
+ binddn = uid=user,....
+ saslmech = [EXTERNAL|PLAIN]
+ tls_cacertdir = /path/to/cadir
+ tls_cert = /path/to/user.crt
+ tls_key = /path/to/user.key
+ tls_reqcert = [never, hard, allow]
+ starttls = [true, false]
+ """
+ path = os.path.expanduser(path)
+ log.debug("dsrc path: %s" % path)
+ # First read our config
+ # No such file?
+ config = configparser.SafeConfigParser()
+ config.read([path])
+
+ log.debug("dsrc instances: %s" % config.sections())
+
+ # Does our section exist?
+ if not config.has_section(instance_name):
+ # If not, return none.
+ log.debug("dsrc no such section %s" % instance_name)
+ return None
+
+ dsrc_inst = {}
+ # Read all the values
+ dsrc_inst['uri'] = config.get(instance_name, 'uri')
+ dsrc_inst['basedn'] = config.get(instance_name, 'basedn',
fallback=None)
+ dsrc_inst['binddn'] = config.get(instance_name, 'binddn',
fallback=None)
+ dsrc_inst['saslmech'] = config.get(instance_name, 'saslmech',
fallback=None)
+ if dsrc_inst['saslmech'] is not None and dsrc_inst['saslmech'] not in
['EXTERNAL', 'PLAIN']:
+ raise Exception("~/.dsrc [%s] saslmech must be one of EXTERNAL or
PLAIN" % instance_name)
+
+ dsrc_inst['tls_cacertdir'] = config.get(instance_name,
'tls_cacertdir', fallback=None)
+ dsrc_inst['tls_cert'] = config.get(instance_name, 'tls_cert',
fallback=None)
+ dsrc_inst['tls_key'] = config.get(instance_name, 'tls_key',
fallback=None)
+ dsrc_inst['tls_reqcert'] = config.get(instance_name, 'tls_reqcert',
fallback='hard')
+ if dsrc_inst['tls_reqcert'] not in ['never', 'allow',
'hard']:
+ raise Exception("dsrc tls_reqcert value invalid. ~/.dsrc [%s] tls_reqcert
should be one of never, allow or hard" % instance_name)
+ if dsrc_inst['tls_reqcert'] == 'never':
+ dsrc_inst['tls_reqcert'] = ldap.OPT_X_TLS_NEVER
+ elif dsrc_inst['tls_reqcert'] == 'allow':
+ dsrc_inst['tls_reqcert'] = ldap.OPT_X_TLS_ALLOW
+ else:
+ dsrc_inst['tls_reqcert'] = ldap.OPT_X_TLS_HARD
+ dsrc_inst['starttls'] = config.getboolean(instance_name, 'starttls',
fallback=False)
+
+ # Return the dict.
+ log.debug("dsrc completed with %s" % dsrc_inst)
+ return dsrc_inst
+
+
diff --git a/src/lib389/lib389/cli_conf/__init__.py
b/src/lib389/lib389/cli_conf/__init__.py
new file mode 100644
index 0000000..d57ac33
--- /dev/null
+++ b/src/lib389/lib389/cli_conf/__init__.py
@@ -0,0 +1,8 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
diff --git a/src/lib389/lib389/cli_conf/backend.py
b/src/lib389/lib389/cli_conf/backend.py
new file mode 100644
index 0000000..e99cc82
--- /dev/null
+++ b/src/lib389/lib389/cli_conf/backend.py
@@ -0,0 +1,76 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389.backend import Backend, Backends
+import argparse
+
+from lib389.cli_base import (
+ _generic_list,
+ _generic_get,
+ _generic_get_dn,
+ _generic_create,
+ _generic_delete,
+ _get_arg,
+ _get_args,
+ _get_attributes,
+ _warn,
+ )
+
+SINGULAR = Backend
+MANY = Backends
+RDN = 'cn'
+
+def backend_list(inst, basedn, log, args):
+ _generic_list(inst, basedn, log.getChild('backend_list'), MANY)
+
+def backend_get(inst, basedn, log, args):
+ rdn = _get_arg( args.selector, msg="Enter %s to retrieve" % RDN)
+ _generic_get(inst, basedn, log.getChild('backend_get'), MANY, rdn)
+
+def backend_get_dn(inst, basedn, log, args):
+ dn = _get_arg( args.dn, msg="Enter dn to retrieve")
+ _generic_get_dn(inst, basedn, log.getChild('backend_get_dn'), MANY, dn)
+
+def backend_create(inst, basedn, log, args):
+ kwargs = _get_attributes(args.extra, SINGULAR._must_attributes)
+ _generic_create(inst, basedn, log.getChild('backend_create'), MANY, kwargs)
+
+def backend_delete(inst, basedn, log, args, warn=True):
+ dn = _get_arg( args.dn, msg="Enter dn to delete")
+ if warn:
+ _warn(dn, msg="Deleting %s %s" % (SINGULAR.__name__, dn))
+ _generic_delete(inst, basedn, log.getChild('backend_delete'), SINGULAR, dn)
+
+
+def create_parser(subparsers):
+ backend_parser = subparsers.add_parser('backend', help="Manage database
suffixes and backends")
+
+ subcommands = backend_parser.add_subparsers(help="action")
+
+ list_parser = subcommands.add_parser('list', help="List current active
backends and suffixes")
+ list_parser.set_defaults(func=backend_list)
+
+ get_parser = subcommands.add_parser('get', help='get')
+ get_parser.set_defaults(func=backend_get)
+ get_parser.add_argument('selector', nargs='?', help='The backend
to search for')
+
+ get_dn_parser = subcommands.add_parser('get_dn', help='get_dn')
+ get_dn_parser.set_defaults(func=backend_get_dn)
+ get_dn_parser.add_argument('dn', nargs='?', help='The backend dn
to get')
+
+ create_parser = subcommands.add_parser('create', help='create')
+ create_parser.set_defaults(func=backend_create)
+ create_parser.add_argument('extra', nargs=argparse.REMAINDER,
+ help='A create may take one or more extra arguments. This parameter
provides them'
+ )
+
+ delete_parser = subcommands.add_parser('delete', help='deletes the
object')
+ delete_parser.set_defaults(func=backend_delete)
+ delete_parser.add_argument('dn', nargs='?', help='The dn to
delete')
+
+
diff --git a/src/lib389/lib389/cli_conf/health.py b/src/lib389/lib389/cli_conf/health.py
new file mode 100644
index 0000000..21c41bc
--- /dev/null
+++ b/src/lib389/lib389/cli_conf/health.py
@@ -0,0 +1,61 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import argparse
+
+from lib389.backend import Backend, Backends
+from lib389.config import Encryption, Config
+from lib389 import plugins
+
+# These get all instances, then check them all.
+CHECK_MANY_OBJECTS = [
+ Backends,
+]
+
+# These get single instances and check them.
+CHECK_OBJECTS = [
+ Config,
+ Encryption,
+ plugins.ReferentialIntegrityPlugin
+]
+
+def _format_check_output(log, result):
+ log.info("==== DS Lint Error: %s ====" % result['dsle'])
+ log.info(" Severity: %s " % result['severity'])
+ log.info(" Affects:")
+ for item in result['items']:
+ log.info(" -- %s" % item)
+ log.info(" Details:")
+ log.info(result['detail'])
+ log.info(" Resolution:")
+ log.info(result['fix'])
+
+def health_check_run(inst, basedn, log, args):
+ log.info("Beginning lint report, this could take a while ...")
+ report = []
+ for lo in CHECK_MANY_OBJECTS:
+ log.info("Checking %s ..." % lo.__name__)
+ lo_inst = lo(inst)
+ for clo in lo_inst.list():
+ result = clo.lint()
+ if result is not None:
+ report += result
+ for lo in CHECK_OBJECTS:
+ log.info("Checking %s ..." % lo.__name__)
+ lo_inst = lo(inst)
+ result = lo_inst.lint()
+ if result is not None:
+ report += result
+ log.info("Healthcheck complete!")
+ for item in report:
+ _format_check_output(log, item)
+
+def create_parser(subparsers):
+ run_healthcheck_parser = subparsers.add_parser('healthcheck', help="Run
a healthcheck report on your Directory Server instance. This is a safe, read only
operation.")
+ run_healthcheck_parser.set_defaults(func=health_check_run)
+
diff --git a/src/lib389/lib389/cli_conf/plugin.py b/src/lib389/lib389/cli_conf/plugin.py
new file mode 100644
index 0000000..b856786
--- /dev/null
+++ b/src/lib389/lib389/cli_conf/plugin.py
@@ -0,0 +1,124 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389.plugins import Plugin, Plugins
+import argparse
+
+from lib389.cli_base import (
+ _generic_list,
+ _generic_get,
+ _generic_get_dn,
+ _generic_create,
+ _generic_delete,
+ _get_arg,
+ _get_args,
+ _get_attributes,
+ _warn,
+ )
+
+SINGULAR = Plugin
+MANY = Plugins
+RDN = 'cn'
+
+
+def plugin_list(inst, basedn, log, args):
+ _generic_list(inst, basedn, log.getChild('plugin_list'), MANY)
+
+def plugin_get(inst, basedn, log, args):
+ rdn = _get_arg( args.selector, msg="Enter %s to retrieve" % RDN)
+ _generic_get(inst, basedn, log.getChild('plugin_get'), MANY, rdn)
+
+def plugin_get_dn(inst, basedn, log, args):
+ dn = _get_arg( args.dn, msg="Enter dn to retrieve")
+ _generic_get_dn(inst, basedn, log.getChild('plugin_get_dn'), MANY, dn)
+
+# Plugin enable
+def plugin_enable(inst, basedn, log, args):
+ dn = _get_arg( args.dn, msg="Enter plugin dn to enable")
+ mc = MANY(inst, basedn)
+ o = mc.get(dn=dn)
+ o.enable()
+ o_str = o.display()
+ log.info('Enabled %s', o_str)
+
+# Plugin disable
+def plugin_disable(inst, basedn, log, args, warn=True):
+ dn = _get_arg( args.dn, msg="Enter plugin dn to disable")
+ if warn:
+ _warn(dn, msg="Disabling %s %s" % (SINGULAR.__name__, dn))
+ mc = MANY(inst, basedn)
+ o = mc.get(dn=dn)
+ o.disable()
+ o_str = o.display()
+ log.info('Disabled %s', o_str)
+
+# Plugin configure?
+def plugin_configure(inst, basedn, log, args):
+ pass
+
+def generic_show(inst, basedn, log, args):
+ """Display plugin configuration."""
+ plugin = args.plugin_cls(inst)
+ log.info(plugin.display())
+
+def generic_enable(inst, basedn, log, args):
+ plugin = args.plugin_cls(inst)
+ plugin.enable()
+ log.info("Enabled %s", plugin.rdn)
+
+def generic_disable(inst, basedn, log, args):
+ plugin = args.plugin_cls(inst)
+ plugin.disable()
+ log.info("Disabled %s", plugin.rdn)
+
+def generic_status(inst, basedn, log, args):
+ plugin = args.plugin_cls(inst)
+ if plugin.status() == True:
+ log.info("%s is enabled", plugin.rdn)
+ else:
+ log.info("%s is disabled", plugin.rdn)
+
+def add_generic_plugin_parsers(subparser, plugin_cls):
+ show_parser = subparser.add_parser('show', help='display plugin
configuration')
+ show_parser.set_defaults(func=generic_show, plugin_cls=plugin_cls)
+
+ enable_parser = subparser.add_parser('enable', help='enable plugin')
+ enable_parser.set_defaults(func=generic_enable, plugin_cls=plugin_cls)
+
+ disable_parser = subparser.add_parser('disable', help='disable
plugin')
+ disable_parser.set_defaults(func=generic_disable, plugin_cls=plugin_cls)
+
+ status_parser = subparser.add_parser('status', help='display plugin
status')
+ status_parser.set_defaults(func=generic_status, plugin_cls=plugin_cls)
+
+
+def create_parser(subparsers):
+ plugin_parser = subparsers.add_parser('plugin', help="Manage plugins
available on the server")
+
+ subcommands = plugin_parser.add_subparsers(help="action")
+
+ list_parser = subcommands.add_parser('list', help="List current
configured (enabled and disabled) plugins")
+ list_parser.set_defaults(func=plugin_list)
+
+ get_parser = subcommands.add_parser('get', help='get')
+ get_parser.set_defaults(func=plugin_get)
+ get_parser.add_argument('selector', nargs='?', help='The plugin
to search for')
+
+ get_dn_parser = subcommands.add_parser('get_dn', help='get_dn')
+ get_dn_parser.set_defaults(func=plugin_get_dn)
+ get_dn_parser.add_argument('dn', nargs='?', help='The plugin dn
to get')
+
+ enable_parser = subcommands.add_parser('enable', help='enable a plugin in
the server')
+ enable_parser.set_defaults(func=plugin_enable)
+ enable_parser.add_argument('dn', nargs='?', help='The dn to
enable')
+
+ disable_parser = subcommands.add_parser('disable', help='disable the
plugin configuration')
+ disable_parser.set_defaults(func=plugin_disable)
+ disable_parser.add_argument('dn', nargs='?', help='The dn to
disable')
+
+
diff --git a/src/lib389/lib389/cli_conf/plugins/__init__.py
b/src/lib389/lib389/cli_conf/plugins/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib389/lib389/cli_conf/plugins/memberof.py
b/src/lib389/lib389/cli_conf/plugins/memberof.py
new file mode 100644
index 0000000..74287f9
--- /dev/null
+++ b/src/lib389/lib389/cli_conf/plugins/memberof.py
@@ -0,0 +1,276 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016-2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+
+from lib389.plugins import MemberOfPlugin
+from lib389.cli_conf.plugin import add_generic_plugin_parsers
+
+
+def manage_attr(inst, basedn, log, args):
+ if args.value is not None:
+ set_attr(inst, basedn, log, args)
+ else:
+ display_attr(inst, basedn, log, args)
+
+def display_attr(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ log.info(plugin.get_attr_formatted())
+
+def set_attr(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ try:
+ plugin.set_attr(args.value)
+ except ldap.UNWILLING_TO_PERFORM:
+ log.error('Error: Illegal value "{}". Failed to
set.'.format(args.value))
+ else:
+ log.info('memberOfAttr set to "{}"'.format(args.value))
+
+def display_groupattr(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ log.info(plugin.get_groupattr_formatted())
+
+def add_groupattr(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ try:
+ plugin.add_groupattr(args.value)
+ except ldap.UNWILLING_TO_PERFORM:
+ log.error('Error: Illegal value "{}". Failed to
add.'.format(args.value))
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ log.info('Value "{}" already exists.'.format(args.value))
+ else:
+ log.info('successfully added memberOfGroupAttr value
"{}"'.format(args.value))
+
+def remove_groupattr(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ try:
+ plugin.remove_groupattr(args.value)
+ except ldap.UNWILLING_TO_PERFORM:
+ log.error("Error: Failed to delete. memberOfGroupAttr is required.")
+ except ldap.NO_SUCH_ATTRIBUTE:
+ log.error('Error: Failed to delete. No value "{0}"
found.'.format(args.value))
+ else:
+ log.info('successfully removed memberOfGroupAttr value
"{}"'.format(args.value))
+
+def display_allbackends(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ val = plugin.get_allbackends_formatted()
+ if not val:
+ log.info("memberOfAllBackends is not set")
+ else:
+ log.info(val)
+
+def enable_allbackends(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ plugin.enable_allbackends()
+ log.info("memberOfAllBackends enabled successfully")
+
+def disable_allbackends(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ plugin.disable_allbackends()
+ log.info("memberOfAllBackends disabled successfully")
+
+def display_skipnested(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ val = plugin.get_skipnested_formatted()
+ if not val:
+ log.info("memberOfSkipNested is not set")
+ else:
+ log.info(val)
+
+def enable_skipnested(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ plugin.enable_skipnested()
+ log.info("memberOfSkipNested set successfully")
+
+def disable_skipnested(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ plugin.disable_skipnested()
+ log.info("memberOfSkipNested unset successfully")
+
+def manage_autoaddoc(inst, basedn, log, args):
+ if args.value == "del":
+ remove_autoaddoc(inst, basedn, log, args)
+ elif args.value is not None:
+ set_autoaddoc(inst, basedn, log, args)
+ else:
+ display_autoaddoc(inst, basedn, log, args)
+
+def display_autoaddoc(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ val = plugin.get_autoaddoc_formatted()
+ if not val:
+ log.info("memberOfAutoAddOc is not set")
+ else:
+ log.info(val)
+
+def set_autoaddoc(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ d = {'nsmemberof': 'nsMemberOf', 'inetuser':
'inetUser', 'inetadmin': 'inetAdmin'}
+ plugin.set_autoaddoc(d[args.value])
+ log.info('memberOfAutoAddOc set to "{}"'.format(d[args.value]))
+
+def remove_autoaddoc(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ if not plugin.get_autoaddoc():
+ log.info("memberOfAutoAddOc was not set")
+ else:
+ plugin.remove_autoaddoc()
+ log.info("memberOfAutoAddOc attribute deleted")
+
+def display_scope(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ val = plugin.get_entryscope_formatted()
+ if not val:
+ log.info("memberOfEntryScope is not set")
+ else:
+ log.info(val)
+
+def add_scope(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ try:
+ plugin.add_entryscope(args.value)
+ except ldap.UNWILLING_TO_PERFORM as ex:
+ if "is also listed as an exclude suffix" in
ex.args[0]['info']:
+ log.error('Error: Include suffix ({0}) is also listed as an exclude
suffix.'.format(args.value))
+ else:
+ log.error('Error: Invalid DN "{}". Failed to
add.'.format(args.value))
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ log.info('Value "{}" already exists.'.format(args.value))
+ else:
+ log.info('successfully added memberOfEntryScope value
"{}"'.format(args.value))
+
+def remove_scope(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ try:
+ plugin.remove_entryscope(args.value)
+ except ldap.NO_SUCH_ATTRIBUTE:
+ log.error('Error: Failed to delete. No value "{0}"
found.'.format(args.value))
+ else:
+ log.info('successfully removed memberOfEntryScope value
"{}"'.format(args.value))
+
+def remove_all_scope(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ plugin.remove_all_entryscope()
+ log.info('successfully removed all memberOfEntryScope values')
+
+def display_excludescope(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ val = plugin.get_excludescope_formatted()
+ if not val:
+ log.info("memberOfEntryScopeExcludeSubtree is not set")
+ else:
+ log.info(val)
+
+def add_excludescope(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ try:
+ plugin.add_excludescope(args.value)
+ except ldap.UNWILLING_TO_PERFORM as ex:
+ if "is also listed as an exclude suffix" in
ex.args[0]['info']:
+ log.error('Error: Suffix ({0}) is listed in entry
scope.'.format(args.value))
+ else:
+ log.error('Error: Invalid DN "{}". Failed to
add.'.format(args.value))
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ log.info('Value "{}" already exists.'.format(args.value))
+ else:
+ log.info('successfully added memberOfEntryScopeExcludeSubtree value
"{}"'.format(args.value))
+
+def remove_excludescope(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ try:
+ plugin.remove_excludescope(args.value)
+ except ldap.NO_SUCH_ATTRIBUTE:
+ log.error('Error: Failed to delete. No value "{0}"
found.'.format(args.value))
+ else:
+ log.info('successfully removed memberOfEntryScopeExcludeSubtree value
"{}"'.format(args.value))
+
+def remove_all_excludescope(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ plugin.remove_all_excludescope()
+ log.info('successfully removed all memberOfEntryScopeExcludeSubtree values')
+
+def fixup(inst, basedn, log, args):
+ plugin = MemberOfPlugin(inst)
+ log.info('Attempting to add task entry... This will fail if MemberOf plug-in is
not enabled.')
+ fixup_task = plugin.fixup(args.basedn, args.filter)
+ log.info('Successfully added task entry ' + fixup_task.dn)
+
+def create_parser(subparsers):
+ memberof_parser = subparsers.add_parser('memberof', help='Manage and
configure MemberOf plugin')
+
+ subcommands = memberof_parser.add_subparsers(help='action')
+
+ add_generic_plugin_parsers(subcommands, MemberOfPlugin)
+
+ attr_parser = subcommands.add_parser('attr', help='get or set
memberofattr')
+ attr_parser.set_defaults(func=manage_attr)
+ attr_parser.add_argument('value', nargs='?', help='The value to
set as memberofattr')
+
+ groupattr_parser = subcommands.add_parser('groupattr', help='get or
manage memberofgroupattr')
+ groupattr_parser.set_defaults(func=display_groupattr)
+ groupattr_subcommands = groupattr_parser.add_subparsers(help='action')
+ add_groupattr_parser = groupattr_subcommands.add_parser('add', help='add
memberofgroupattr value')
+ add_groupattr_parser.set_defaults(func=add_groupattr)
+ add_groupattr_parser.add_argument('value', help='The value to add in
memberofgroupattr')
+ del_groupattr_parser = groupattr_subcommands.add_parser('del',
help='remove memberofgroupattr value')
+ del_groupattr_parser.set_defaults(func=remove_groupattr)
+ del_groupattr_parser.add_argument('value', help='The value to remove from
memberofgroupattr')
+
+ allbackends_parser = subcommands.add_parser('allbackends', help='get or
manage memberofallbackends')
+ allbackends_parser.set_defaults(func=display_allbackends)
+ allbackends_subcommands = allbackends_parser.add_subparsers(help='action')
+ on_allbackends_parser = allbackends_subcommands.add_parser('on',
help='enable all backends for memberof')
+ on_allbackends_parser.set_defaults(func=enable_allbackends)
+ off_allbackends_parser = allbackends_subcommands.add_parser('off',
help='disable all backends for memberof')
+ off_allbackends_parser.set_defaults(func=disable_allbackends)
+
+ skipnested_parser = subcommands.add_parser('skipnested', help='get or
manage memberofskipnested')
+ skipnested_parser.set_defaults(func=display_skipnested)
+ skipnested_subcommands = skipnested_parser.add_subparsers(help='action')
+ on_skipnested_parser = skipnested_subcommands.add_parser('on', help='skip
nested groups for memberof')
+ on_skipnested_parser.set_defaults(func=enable_skipnested)
+ off_skipnested_parser = skipnested_subcommands.add_parser('off',
help="don't skip nested groups for memberof")
+ off_skipnested_parser.set_defaults(func=disable_skipnested)
+
+ autoaddoc_parser = subcommands.add_parser('autoaddoc', help='get or set
memberofautoaddoc')
+ autoaddoc_parser.set_defaults(func=manage_autoaddoc)
+ autoaddoc_parser.add_argument('value', nargs='?',
choices=['nsmemberof', 'inetuser', 'inetadmin', 'del'],
+ type=str.lower, help='The value to set as
memberofautoaddoc or del to remove the attribute')
+
+ scope_parser = subcommands.add_parser('scope', help='get or manage
memberofentryscope')
+ scope_parser.set_defaults(func=display_scope)
+ scope_subcommands = scope_parser.add_subparsers(help='action')
+ add_scope_parser = scope_subcommands.add_parser('add', help='add
memberofentryscope value')
+ add_scope_parser.set_defaults(func=add_scope)
+ add_scope_parser.add_argument('value', help='The value to add in
memberofentryscope')
+ del_scope_parser = scope_subcommands.add_parser('del', help='remove
memberofentryscope value')
+ del_scope_parser.set_defaults(func=remove_scope)
+ del_scope_parser.add_argument('value', help='The value to remove from
memberofentryscope')
+ delall_scope_parser = scope_subcommands.add_parser('delall', help='remove
all memberofentryscope values')
+ delall_scope_parser.set_defaults(func=remove_all_scope)
+
+ exclude_parser = subcommands.add_parser('exclude', help='get or manage
memberofentryscopeexcludesubtree')
+ exclude_parser.set_defaults(func=display_excludescope)
+ exclude_subcommands = exclude_parser.add_subparsers(help='action')
+ add_exclude_parser = exclude_subcommands.add_parser('add', help='add
memberofentryscopeexcludesubtree value')
+ add_exclude_parser.set_defaults(func=add_excludescope)
+ add_exclude_parser.add_argument('value', help='The value to add in
memberofentryscopeexcludesubtree')
+ del_exclude_parser = exclude_subcommands.add_parser('del', help='remove
memberofentryscopeexcludesubtree value')
+ del_exclude_parser.set_defaults(func=remove_excludescope)
+ del_exclude_parser.add_argument('value', help='The value to remove from
memberofentryscopeexcludesubtree')
+ delall_exclude_parser = exclude_subcommands.add_parser('delall',
help='remove all memberofentryscopeexcludesubtree values')
+ delall_exclude_parser.set_defaults(func=remove_all_excludescope)
+
+ fixup_parser = subcommands.add_parser('fixup', help='run the fix-up task
for memberof plugin')
+ fixup_parser.set_defaults(func=fixup)
+ fixup_parser.add_argument('-b', '--basedn', required=True,
help="base DN that contains entries to fix up")
+ fixup_parser.add_argument('-f', '--filter', help="Filter for
entries to fix up.\n"
+ "If omitted, all entries with objectclass inetuser/inetadmin/nsmemberof
under the\n"
+ "specified base will have their memberOf attribute regenerated."
+ )
diff --git a/src/lib389/lib389/cli_conf/plugins/referint.py
b/src/lib389/lib389/cli_conf/plugins/referint.py
new file mode 100644
index 0000000..9d52b89
--- /dev/null
+++ b/src/lib389/lib389/cli_conf/plugins/referint.py
@@ -0,0 +1,197 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016-2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+
+from lib389.plugins import ReferentialIntegrityPlugin
+from lib389.cli_conf.plugin import add_generic_plugin_parsers
+
+
+def manage_update_delay(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ if args.value is None:
+ val = plugin.get_update_delay_formatted()
+ log.info(val)
+ else:
+ plugin.set_update_delay(args.value)
+ log.info('referint-update-delay set to
"{}"'.format(args.value))
+
+def display_membership_attr(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ log.info(plugin.get_membership_attr_formatted())
+
+def add_membership_attr(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ try:
+ plugin.add_membership_attr(args.value)
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ log.info('Value "{}" already exists.'.format(args.value))
+ else:
+ log.info('successfully added membership attribute
"{}"'.format(args.value))
+
+def remove_membership_attr(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ try:
+ plugin.remove_membership_attr(args.value)
+ except ldap.OPERATIONS_ERROR:
+ log.error("Error: Failed to delete. At least one value for membership
attribute should exist.")
+ except ldap.NO_SUCH_ATTRIBUTE:
+ log.error('Error: Failed to delete. No value "{0}"
found.'.format(args.value))
+ else:
+ log.info('successfully removed membership attribute
"{}"'.format(args.value))
+
+def display_scope(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ val = plugin.get_entryscope_formatted()
+ if not val:
+ log.info("nsslapd-pluginEntryScope is not set")
+ else:
+ log.info(val)
+
+def add_scope(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ try:
+ plugin.add_entryscope(args.value)
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ log.info('Value "{}" already exists.'.format(args.value))
+ else:
+ log.info('successfully added nsslapd-pluginEntryScope value
"{}"'.format(args.value))
+
+def remove_scope(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ try:
+ plugin.remove_entryscope(args.value)
+ except ldap.NO_SUCH_ATTRIBUTE:
+ log.error('Error: Failed to delete. No value "{0}"
found.'.format(args.value))
+ else:
+ log.info('successfully removed nsslapd-pluginEntryScope value
"{}"'.format(args.value))
+
+def remove_all_scope(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ plugin.remove_all_entryscope()
+ log.info('successfully removed all nsslapd-pluginEntryScope values')
+
+def display_excludescope(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ val = plugin.get_excludescope_formatted()
+ if not val:
+ log.info("nsslapd-pluginExcludeEntryScope is not set")
+ else:
+ log.info(val)
+
+def add_excludescope(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ try:
+ plugin.add_excludescope(args.value)
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ log.info('Value "{}" already exists.'.format(args.value))
+ else:
+ log.info('successfully added nsslapd-pluginExcludeEntryScope value
"{}"'.format(args.value))
+
+def remove_excludescope(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ try:
+ plugin.remove_excludescope(args.value)
+ except ldap.NO_SUCH_ATTRIBUTE:
+ log.error('Error: Failed to delete. No value "{0}"
found.'.format(args.value))
+ else:
+ log.info('successfully removed nsslapd-pluginExcludeEntryScope value
"{}"'.format(args.value))
+
+def remove_all_excludescope(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ plugin.remove_all_excludescope()
+ log.info('successfully removed all nsslapd-pluginExcludeEntryScope values')
+
+def display_container_scope(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ val = plugin.get_container_scope_formatted()
+ if not val:
+ log.info("nsslapd-pluginContainerScope is not set")
+ else:
+ log.info(val)
+
+def add_container_scope(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ try:
+ plugin.add_container_scope(args.value)
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ log.info('Value "{}" already exists.'.format(args.value))
+ else:
+ log.info('successfully added nsslapd-pluginContainerScope value
"{}"'.format(args.value))
+
+def remove_container_scope(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ try:
+ plugin.remove_container_scope(args.value)
+ except ldap.NO_SUCH_ATTRIBUTE:
+ log.error('Error: Failed to delete. No value "{0}"
found.'.format(args.value))
+ else:
+ log.info('successfully removed nsslapd-pluginContainerScope value
"{}"'.format(args.value))
+
+def remove_all_container_scope(inst, basedn, log, args):
+ plugin = ReferentialIntegrityPlugin(inst)
+ plugin.remove_all_container_scope()
+ log.info('successfully removed all nsslapd-pluginContainerScope values')
+
+
+def create_parser(subparsers):
+ referint_parser = subparsers.add_parser('referint', help='Manage and
configure Referential Integrity plugin')
+
+ subcommands = referint_parser.add_subparsers(help='action')
+
+ add_generic_plugin_parsers(subcommands, ReferentialIntegrityPlugin)
+
+ delay_parser = subcommands.add_parser('delay', help='get or set update
delay')
+ delay_parser.set_defaults(func=manage_update_delay)
+ delay_parser.add_argument('value', nargs='?', help='The value to
set as update delay')
+
+ attr_parser = subcommands.add_parser('attrs', help='get or manage
membership attributes')
+ attr_parser.set_defaults(func=display_membership_attr)
+ attr_subcommands = attr_parser.add_subparsers(help='action')
+ add_attr_parser = attr_subcommands.add_parser('add', help='add membership
attribute')
+ add_attr_parser.set_defaults(func=add_membership_attr)
+ add_attr_parser.add_argument('value', help='membership attribute to
add')
+ del_attr_parser = attr_subcommands.add_parser('del', help='remove
membership attribute')
+ del_attr_parser.set_defaults(func=remove_membership_attr)
+ del_attr_parser.add_argument('value', help='membership attribute to
remove')
+
+ scope_parser = subcommands.add_parser('scope', help='get or manage
referint scope')
+ scope_parser.set_defaults(func=display_scope)
+ scope_subcommands = scope_parser.add_subparsers(help='action')
+ add_scope_parser = scope_subcommands.add_parser('add', help='add entry
scope value')
+ add_scope_parser.set_defaults(func=add_scope)
+ add_scope_parser.add_argument('value', help='The value to add in referint
entry scope')
+ del_scope_parser = scope_subcommands.add_parser('del', help='remove entry
scope value')
+ del_scope_parser.set_defaults(func=remove_scope)
+ del_scope_parser.add_argument('value', help='The value to remove from
entry scope')
+ delall_scope_parser = scope_subcommands.add_parser('delall', help='remove
all entry scope values')
+ delall_scope_parser.set_defaults(func=remove_all_scope)
+
+ exclude_parser = subcommands.add_parser('exclude', help='get or manage
referint exclude scope')
+ exclude_parser.set_defaults(func=display_excludescope)
+ exclude_subcommands = exclude_parser.add_subparsers(help='action')
+ add_exclude_parser = exclude_subcommands.add_parser('add', help='add
exclude scope value')
+ add_exclude_parser.set_defaults(func=add_excludescope)
+ add_exclude_parser.add_argument('value', help='The value to add in
exclude scope')
+ del_exclude_parser = exclude_subcommands.add_parser('del', help='remove
exclude scope value')
+ del_exclude_parser.set_defaults(func=remove_excludescope)
+ del_exclude_parser.add_argument('value', help='The value to remove from
exclude scope')
+ delall_exclude_parser = exclude_subcommands.add_parser('delall',
help='remove all exclude scope values')
+ delall_exclude_parser.set_defaults(func=remove_all_excludescope)
+
+ container_parser = subcommands.add_parser('container', help='get or
manage referint container scope')
+ container_parser.set_defaults(func=display_container_scope)
+ container_subcommands = container_parser.add_subparsers(help='action')
+ add_container_parser = container_subcommands.add_parser('add', help='add
container scope value')
+ add_container_parser.set_defaults(func=add_container_scope)
+ add_container_parser.add_argument('value', help='The value to add in
container scope')
+ del_container_parser = container_subcommands.add_parser('del',
help='remove container scope value')
+ del_container_parser.set_defaults(func=remove_container_scope)
+ del_container_parser.add_argument('value', help='The value to remove from
container scope')
+ delall_container_parser = container_subcommands.add_parser('delall',
help='remove all container scope values')
+ delall_container_parser.set_defaults(func=remove_all_container_scope)
diff --git a/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py
b/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py
new file mode 100644
index 0000000..1e11504
--- /dev/null
+++ b/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py
@@ -0,0 +1,229 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+
+from lib389.plugins import RootDNAccessControlPlugin
+from lib389.cli_conf.plugin import add_generic_plugin_parsers
+
+
+def display_time(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+ val = plugin.get_open_time_formatted()
+ if not val:
+ log.info("rootdn-open-time is not set")
+ else:
+ log.info(val)
+ val = plugin.get_close_time_formatted()
+ if not val:
+ log.info("rootdn-close-time is not set")
+ else:
+ log.info(val)
+
+def set_open_time(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+ plugin.set_open_time(args.value)
+ log.info('rootdn-open-time set to "{}"'.format(args.value))
+
+def set_close_time(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+ plugin.set_close_time(args.value)
+ log.info('rootdn-close-time set to "{}"'.format(args.value))
+
+def clear_time(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+ plugin.remove_open_time()
+ plugin.remove_close_time()
+ log.info('time-based policy was cleared')
+
+def display_ips(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+ allowed_ips = plugin.get_allow_ip_formatted()
+ denied_ips = plugin.get_deny_ip_formatted()
+ if not allowed_ips and not denied_ips:
+ log.info("No ip-based access control policy has been configured")
+ else:
+ log.info(allowed_ips)
+ log.info(denied_ips)
+
+def allow_ip(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+
+ # remove ip from denied ips
+ try:
+ plugin.remove_deny_ip(args.value)
+ except ldap.NO_SUCH_ATTRIBUTE:
+ pass
+
+ try:
+ plugin.add_allow_ip(args.value)
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ pass
+ log.info('{} added to rootdn-allow-ip'.format(args.value))
+
+def deny_ip(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+
+ # remove ip from allowed ips
+ try:
+ plugin.remove_allow_ip(args.value)
+ except ldap.NO_SUCH_ATTRIBUTE:
+ pass
+
+ try:
+ plugin.add_deny_ip(args.value)
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ pass
+ log.info('{} added to rootdn-deny-ip'.format(args.value))
+
+def clear_all_ips(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+ plugin.remove_all_allow_ip()
+ plugin.remove_all_deny_ip()
+ log.info('ip-based policy was cleared')
+
+def display_hosts(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+ allowed_hosts = plugin.get_allow_host_formatted()
+ denied_hosts = plugin.get_deny_host_formatted()
+ if not allowed_hosts and not denied_hosts:
+ log.info("No host-based access control policy has been configured")
+ else:
+ log.info(allowed_hosts)
+ log.info(denied_hosts)
+
+def allow_host(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+
+ # remove host from denied hosts
+ try:
+ plugin.remove_deny_host(args.value)
+ except ldap.NO_SUCH_ATTRIBUTE:
+ pass
+
+ try:
+ plugin.add_allow_host(args.value)
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ pass
+ log.info('{} added to rootdn-allow-host'.format(args.value))
+
+def deny_host(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+
+ # remove host from allowed hosts
+ try:
+ plugin.remove_allow_host(args.value)
+ except ldap.NO_SUCH_ATTRIBUTE:
+ pass
+
+ try:
+ plugin.add_deny_host(args.value)
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ pass
+ log.info('{} added to rootdn-deny-host'.format(args.value))
+
+def clear_all_hosts(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+ plugin.remove_all_allow_host()
+ plugin.remove_all_deny_host()
+ log.info('host-based policy was cleared')
+
+def display_days(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+ days = plugin.get_days_allowed_formatted()
+ if not days:
+ log.info("No day-based access control policy has been configured")
+ else:
+ log.info(days)
+
+def allow_day(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+ args.value = args.value[0:3]
+ plugin.add_allow_day(args.value)
+ log.info('{} added to rootdn-days-allowed'.format(args.value))
+
+def deny_day(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+ args.value = args.value[0:3]
+ plugin.remove_allow_day(args.value)
+ log.info('{} removed from rootdn-days-allowed'.format(args.value))
+
+def clear_all_days(inst, basedn, log, args):
+ plugin = RootDNAccessControlPlugin(inst)
+ plugin.remove_days_allowed()
+ log.info('day-based policy was cleared')
+
+
+def create_parser(subparsers):
+ rootdnac_parser = subparsers.add_parser('rootdn', help='Manage and
configure RootDN Access Control plugin')
+ subcommands = rootdnac_parser.add_subparsers(help='action')
+ add_generic_plugin_parsers(subcommands, RootDNAccessControlPlugin)
+
+ time_parser = subcommands.add_parser('time', help='get or set rootdn open
and close times')
+ time_parser.set_defaults(func=display_time)
+
+ time_subcommands = time_parser.add_subparsers(help='action')
+
+ open_time_parser = time_subcommands.add_parser('open', help='set open
time value')
+ open_time_parser.set_defaults(func=set_open_time)
+ open_time_parser.add_argument('value', help='Value to set as open
time')
+
+ close_time_parser = time_subcommands.add_parser('close', help='set close
time value')
+ close_time_parser.set_defaults(func=set_close_time)
+ close_time_parser.add_argument('value', help='Value to set as close
time')
+
+ time_clear_parser = time_subcommands.add_parser('clear', help='reset
time-based access policy')
+ time_clear_parser.set_defaults(func=clear_time)
+
+ ip_parser = subcommands.add_parser('ip', help='get or set ip access
policy')
+ ip_parser.set_defaults(func=display_ips)
+
+ ip_subcommands = ip_parser.add_subparsers(help='action')
+
+ ip_allow_parser = ip_subcommands.add_parser('allow', help='allow IP addr
or IP addr range')
+ ip_allow_parser.set_defaults(func=allow_ip)
+ ip_allow_parser.add_argument('value', help='IP addr or IP addr
range')
+
+ ip_deny_parser = ip_subcommands.add_parser('deny', help='deny IP addr or
IP addr range')
+ ip_deny_parser.set_defaults(func=deny_ip)
+ ip_deny_parser.add_argument('value', help='IP addr or IP addr
range')
+
+ ip_clear_parser = ip_subcommands.add_parser('clear', help='reset IP-based
access policy')
+ ip_clear_parser.set_defaults(func=clear_all_ips)
+
+ host_parser = subcommands.add_parser('host', help='get or set host access
policy')
+ host_parser.set_defaults(func=display_hosts)
+
+ host_subcommands = host_parser.add_subparsers(help='action')
+
+ host_allow_parser = host_subcommands.add_parser('allow', help='allow host
address')
+ host_allow_parser.set_defaults(func=allow_host)
+ host_allow_parser.add_argument('value', help='host address')
+
+ host_deny_parser = host_subcommands.add_parser('deny', help='deny host
address')
+ host_deny_parser.set_defaults(func=deny_host)
+ host_deny_parser.add_argument('value', help='host address')
+
+ host_clear_parser = host_subcommands.add_parser('clear', help='reset
host-based access policy')
+ host_clear_parser.set_defaults(func=clear_all_hosts)
+
+ day_parser = subcommands.add_parser('day', help='get or set days access
policy')
+ day_parser.set_defaults(func=display_days)
+
+ day_subcommands = day_parser.add_subparsers(help='action')
+
+ day_allow_parser = day_subcommands.add_parser('allow', help='allow day of
the week')
+ day_allow_parser.set_defaults(func=allow_day)
+ day_allow_parser.add_argument('value', type=str.capitalize, help='day of
the week')
+
+ day_deny_parser = day_subcommands.add_parser('deny', help='deny day of
the week')
+ day_deny_parser.set_defaults(func=deny_day)
+ day_deny_parser.add_argument('value', type=str.capitalize, help='day of
the week')
+
+ day_clear_parser = day_subcommands.add_parser('clear', help='reset
day-based access policy')
+ day_clear_parser.set_defaults(func=clear_all_days)
diff --git a/src/lib389/lib389/cli_conf/plugins/usn.py
b/src/lib389/lib389/cli_conf/plugins/usn.py
new file mode 100644
index 0000000..05e34dd
--- /dev/null
+++ b/src/lib389/lib389/cli_conf/plugins/usn.py
@@ -0,0 +1,56 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016-2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389.plugins import USNPlugin
+from lib389.cli_conf.plugin import add_generic_plugin_parsers
+
+
+def display_usn_mode(inst, basedn, log, args):
+ plugin = USNPlugin(inst)
+ if plugin.is_global_mode_set():
+ log.info("USN global mode is enabled")
+ else:
+ log.info("USN global mode is disabled")
+
+def enable_global_mode(inst, basedn, log, args):
+ plugin = USNPlugin(inst)
+ plugin.enable_global_mode()
+ log.info("USN global mode enabled")
+
+def disable_global_mode(inst, basedn, log, args):
+ plugin = USNPlugin(inst)
+ plugin.disable_global_mode()
+ log.info("USN global mode disabled")
+
+def tombstone_cleanup(inst, basedn, log, args):
+ plugin = USNPlugin(inst)
+ log.info('Attempting to add task entry... This will fail if replication is
enabled or if USN plug-in is disabled.')
+ task = plugin.cleanup(args.suffix, args.backend, args.maxusn)
+ log.info('Successfully added task entry ' + task.dn)
+
+def create_parser(subparsers):
+ usn_parser = subparsers.add_parser('usn', help='Manage and configure USN
plugin')
+
+ subcommands = usn_parser.add_subparsers(help='action')
+
+ add_generic_plugin_parsers(subcommands, USNPlugin)
+
+ global_mode_parser = subcommands.add_parser('global', help='get or manage
global usn mode')
+ global_mode_parser.set_defaults(func=display_usn_mode)
+ global_mode_subcommands = global_mode_parser.add_subparsers(help='action')
+ on_global_mode_parser = global_mode_subcommands.add_parser('on',
help='enable usn global mode')
+ on_global_mode_parser.set_defaults(func=enable_global_mode)
+ off_global_mode_parser = global_mode_subcommands.add_parser('off',
help='disable usn global mode')
+ off_global_mode_parser.set_defaults(func=disable_global_mode)
+
+ cleanup_parser = subcommands.add_parser('cleanup', help='run the USN
tombstone cleanup task')
+ cleanup_parser.set_defaults(func=tombstone_cleanup)
+ cleanup_group = cleanup_parser.add_mutually_exclusive_group(required=True)
+ cleanup_group.add_argument('-s', '--suffix', help="suffix where
USN tombstone entries are cleaned up")
+ cleanup_group.add_argument('-n', '--backend', help="backend
instance in which USN tombstone entries are cleaned up (alternative to suffix)")
+ cleanup_parser.add_argument('-m', '--maxusn', type=int,
help="USN tombstone entries are deleted up to the entry with maxusn")
diff --git a/src/lib389/lib389/cli_conf/plugins/whoami.py
b/src/lib389/lib389/cli_conf/plugins/whoami.py
new file mode 100644
index 0000000..bdb1e85
--- /dev/null
+++ b/src/lib389/lib389/cli_conf/plugins/whoami.py
@@ -0,0 +1,16 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016-2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389.plugins import WhoamiPlugin
+from lib389.cli_conf.plugin import add_generic_plugin_parsers
+
+
+def create_parser(subparsers):
+ whoami_parser = subparsers.add_parser('whoami', help='Manage and
configure whoami plugin')
+ subcommands = whoami_parser.add_subparsers(help='action')
+ add_generic_plugin_parsers(subcommands, WhoamiPlugin)
diff --git a/src/lib389/lib389/cli_conf/schema.py b/src/lib389/lib389/cli_conf/schema.py
new file mode 100644
index 0000000..49596b5
--- /dev/null
+++ b/src/lib389/lib389/cli_conf/schema.py
@@ -0,0 +1,53 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389.cli_base import _get_arg
+from lib389.schema import Schema
+
+
+def list_attributetype(inst, basedn, log, args):
+ for attributetype in inst.schema.get_attributetypes():
+ print(attributetype)
+
+def query_attributetype(inst, basedn, log, args):
+ # Need the query type
+ attr = _get_arg(args.attr, msg="Enter attribute to query")
+ attributetype, must, may = inst.schema.query_attributetype(attr)
+ print(attributetype)
+ print("")
+ print("MUST")
+ for oc in must:
+ print(oc)
+ print("")
+ print("MAY")
+ for oc in may:
+ print(oc)
+
+def reload_schema(inst, basedn, log, args):
+ schema = Schema(inst)
+ log.info('Attempting to add task entry... This will fail if Schema Reload plug-in
is not enabled.')
+ task = schema.reload(args.schemadir)
+ log.info('Successfully added task entry ' + task.dn)
+ log.info("To verify that the schema reload operation was successful, please
check the error logs.")
+
+
+def create_parser(subparsers):
+ schema_parser = subparsers.add_parser('schema', help='Query and
manipulate schema')
+
+ subcommands = schema_parser.add_subparsers(help='schema')
+
+ list_attributetype_parser = subcommands.add_parser('list_attributetype',
help='List avaliable attribute types on this system')
+ list_attributetype_parser.set_defaults(func=list_attributetype)
+
+ query_attributetype_parser = subcommands.add_parser('query_attributetype',
help='Query an attribute to determine object classes that may or must take it')
+ query_attributetype_parser.set_defaults(func=query_attributetype)
+ query_attributetype_parser.add_argument('attr', nargs='?',
help='Attribute type to query')
+
+ reload_parser = subcommands.add_parser('reload', help='Dynamically reload
schema while server is running')
+ reload_parser.set_defaults(func=reload_schema)
+ reload_parser.add_argument('-d', '--schemadir', help="directory
where schema files are located")
diff --git a/src/lib389/lib389/cli_ctl/__init__.py
b/src/lib389/lib389/cli_ctl/__init__.py
new file mode 100644
index 0000000..d57ac33
--- /dev/null
+++ b/src/lib389/lib389/cli_ctl/__init__.py
@@ -0,0 +1,8 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
diff --git a/src/lib389/lib389/cli_ctl/dbtasks.py b/src/lib389/lib389/cli_ctl/dbtasks.py
new file mode 100644
index 0000000..e41768c
--- /dev/null
+++ b/src/lib389/lib389/cli_ctl/dbtasks.py
@@ -0,0 +1,56 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+def dbtasks_db2index(inst, log, args):
+ inst.db2index(bename=args.backend)
+
+def dbtasks_db2bak(inst, log, args):
+ # Needs an output name?
+ inst.db2bak(args.archive)
+ log.info("db2bak successful")
+
+def dbtasks_bak2db(inst, log, args):
+ # Needs the archive to restore.
+ inst.bak2db(args.archive)
+ log.info("bak2db successful")
+
+def dbtasks_db2ldif(inst, log, args):
+ inst.db2ldif(bename=args.backend, encrypt=args.encrypted, repl_data=args.replication,
outputfile=args.ldif, suffixes=None, excludeSuffixes=None)
+ log.info("db2ldif successful")
+
+def dbtasks_ldif2db(inst, log, args):
+ inst.ldif2db(bename=args.backend, encrypt=args.encrypted, import_file=args.ldif,
suffixes=None, excludeSuffixes=None)
+ log.info("ldif2db successful")
+
+def create_parser(subcommands):
+ db2index_parser = subcommands.add_parser('db2index', help="Initialise a
reindex of the server database. The server must be stopped for this to proceed.")
+ # db2index_parser.add_argument('suffix', help="The suffix to reindex. IE
dc=example,dc=com.")
+ db2index_parser.add_argument('backend', help="The backend to reindex. IE
userRoot")
+ db2index_parser.set_defaults(func=dbtasks_db2index)
+
+ db2bak_parser = subcommands.add_parser('db2bak', help="Initialise a BDB
backup of the database. The server must be stopped for this to proceed.")
+ db2bak_parser.add_argument('archive', help="The destination for the
archive. This will be created during the db2bak process.")
+ db2bak_parser.set_defaults(func=dbtasks_db2bak)
+
+ db2ldif_parser = subcommands.add_parser('db2ldif', help="Initialise an
LDIF dump of the database. The server must be stopped for this to proceed.")
+ db2ldif_parser.add_argument('backend', help="The backend to output as an
LDIF. IE userRoot")
+ db2ldif_parser.add_argument('ldif', help="The path to the ldif output
location.")
+ db2ldif_parser.add_argument('--replication', help="Export replication
information, suitable for importing on a new consumer or backups.", default=False,
action='store_true')
+ db2ldif_parser.add_argument('--encrypted', help="Export encrypted
attributes", default=False, action='store_true')
+ db2ldif_parser.set_defaults(func=dbtasks_db2ldif)
+
+ bak2db_parser = subcommands.add_parser('bak2db', help="Restore a BDB
backup of the database. The server must be stopped for this to proceed.")
+ bak2db_parser.add_argument('archive', help="The archive to restore. This
will erase all current server databases.")
+ bak2db_parser.set_defaults(func=dbtasks_bak2db)
+
+ ldif2db_parser = subcommands.add_parser('ldif2db', help="Restore an LDIF
dump of the database. The server must be stopped for this to proceed.")
+ ldif2db_parser.add_argument('backend', help="The backend to restore from
an LDIF. IE userRoot")
+ ldif2db_parser.add_argument('ldif', help="The path to the ldif to
import")
+ ldif2db_parser.add_argument('--encrypted', help="Import encrypted
attributes", default=False, action='store_true')
+ ldif2db_parser.set_defaults(func=dbtasks_ldif2db)
+
diff --git a/src/lib389/lib389/cli_ctl/instance.py
b/src/lib389/lib389/cli_ctl/instance.py
new file mode 100644
index 0000000..f59fb2b
--- /dev/null
+++ b/src/lib389/lib389/cli_ctl/instance.py
@@ -0,0 +1,133 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389._constants import *
+
+from lib389.tools import DirSrvTools
+from lib389.instance.setup import SetupDs
+from getpass import getpass
+import os
+import time
+import sys
+
+from lib389.instance.options import General2Base, Slapd2Base
+
+def instance_list(inst, log, args):
+ instances = inst.list(all=True)
+
+ try:
+ if len(instances) > 0:
+ for instance in instances:
+ log.info("instance: %s" % instance[CONF_SERVER_ID])
+ else:
+ log.info("No instances of Directory Server")
+ except IOError as e:
+ log.info(e)
+ log.info("Perhaps you need to be a different user?")
+
+def instance_restart(inst, log, args):
+ inst.restart(post_open=False)
+
+def instance_start(inst, log, args):
+ inst.start(post_open=False)
+
+def instance_stop(inst, log, args):
+ inst.stop()
+
+def instance_status(inst, log, args):
+ if inst.status() is True:
+ log.info("Instance is running")
+ else:
+ log.info("Instance is not running")
+
+def instance_create(inst, log, args):
+ if not args.ack:
+ sys.exit(0)
+ else:
+ log.info("""
+ _________________________________________
+/ This is not what you want! Press ctrl-c \\
+\ now ... /
+ -----------------------------------------
+ \\ / \\ //\\
+ \\ |\\___/| / \\// \\\\
+ /0 0 \\__ / // | \\ \\
+ / / \\/_/ // | \\ \\
+ @_^_@'/ \\/_ // | \\ \\
+ //_^_/ \\/_ // | \\ \\
+ ( //) | \\/// | \\ \\
+ ( / /) _|_ / ) // | \\ _\\
+ ( // /) '/,_ _ _/ ( ; -. | _ _\\.-~ .-~~~^-.
+ (( / / )) ,-{ _ `-.|.-~-. .~ `.
+ (( // / )) '/\\ / ~-. _ .-~ .-~^-. \\
+ (( /// )) `. { } / \\ \\
+ (( / )) .----~-.\\ \\-' .~ \\ `. \\^-.
+ ///.----..> \\ _ -~ `. ^-` ^-_
+ ///-._ _ _ _ _ _ _}^ - - - - ~ ~-- ,.-~
+ /.-~
+ """)
+ for i in range(1,6):
+ log.info('%s ...' % (5 - int(i)))
+ time.sleep(1)
+ log.info('Launching ...')
+ if args.containerised:
+ log.debug("Containerised features requested.")
+ sd = SetupDs(args.verbose, args.dryrun, log, args.containerised)
+ ### If args.file is not set, we need to interactively get answers!
+ if sd.create_from_inf(args.file):
+ # print("Sucessfully created instance")
+ return True
+ else:
+ # print("Failed to create instance")
+ return False
+
+def instance_example(inst, log, args):
+ print("""
+; --- BEGIN COPYRIGHT BLOCK ---
+; Copyright (C) 2015 Red Hat, Inc.
+; All rights reserved.
+;
+; License: GPL (version 3 or any later version).
+; See LICENSE for details.
+; --- END COPYRIGHT BLOCK ---
+
+; Author: firstyear at
redhat.com
+
+; This is a version 2 ds setup inf file.
+; It is used by the python versions of setup-ds-*
+; Most options map 1 to 1 to the original .inf file.
+; However, there are some differences that I envision
+; For example, note the split backend section.
+; You should be able to create, one, many or no backends in an install
+;
+; The special value {instance_name} is substituted at installation time.
+;
+
+ """)
+ g2b = General2Base(log)
+ s2b = Slapd2Base(log)
+ print(g2b.collect_help())
+ print(s2b.collect_help())
+
+def create_parser(subcommands):
+ # list_parser = subcommands.add_parser('list', help="List installed
instances of Directory Server")
+ # list_parser.set_defaults(func=instance_list)
+ # list_parser.set_defaults(noinst=True)
+ restart_parser = subcommands.add_parser('restart', help="Restart an
instance of Directory Server, if it is running: else start it.")
+ restart_parser.set_defaults(func=instance_restart)
+
+ start_parser = subcommands.add_parser('start', help="Start an instance
of Directory Server, if it is not currently running")
+ start_parser.set_defaults(func=instance_start)
+
+ stop_parser = subcommands.add_parser('stop', help="Stop an instance of
Directory Server, if it is currently running")
+ stop_parser.set_defaults(func=instance_stop)
+
+ status_parser = subcommands.add_parser('status', help="Check running
status of an instance of Directory Server")
+ status_parser.set_defaults(func=instance_status)
+
+
diff --git a/src/lib389/lib389/cli_idm/__init__.py
b/src/lib389/lib389/cli_idm/__init__.py
new file mode 100644
index 0000000..39cd01d
--- /dev/null
+++ b/src/lib389/lib389/cli_idm/__init__.py
@@ -0,0 +1,114 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from getpass import getpass
+from lib389 import DirSrv
+from lib389.properties import SER_LDAP_URL, SER_ROOT_DN, SER_ROOT_PW
+
+
+def _get_arg(args, msg=None):
+ if args is not None and len(args) > 0:
+ if type(args) is list:
+ return args[0]
+ else:
+ return args
+ else:
+ return raw_input("%s : " % msg)
+
+def _get_args(args, kws):
+ kwargs = {}
+ while len(kws) > 0:
+ kw, msg, priv = kws.pop(0)
+
+ if args is not None and len(args) > 0:
+ kwargs[kw] = args.pop(0)
+ else:
+ if priv:
+ kwargs[kw] = getpass("%s : " % msg)
+ else:
+ kwargs[kw] = raw_input("%s : " % msg)
+ return kwargs
+
+# This is really similar to get_args, but generates from the MUST_ATTRIBUTES array
+def _get_attributes(args, attrs):
+ kwargs = {}
+ for attr in attrs:
+ if args is not None and len(args) > 0:
+ kwargs[attr] = args.pop(0)
+ else:
+ if attr.lower() == 'userpassword':
+ kwargs[attr] = getpass("Enter value for %s : " % attr)
+ else:
+ kwargs[attr] = raw_input("Enter value for %s : " % attr)
+ return kwargs
+
+
+def _warn(data, msg=None):
+ if msg is not None:
+ print("%s :" % msg)
+ if 'Yes I am sure' != raw_input("Type 'Yes I am sure' to
continue: "):
+ raise Exception("Not sure if want")
+ return data
+
+def connect_instance(ldapurl, binddn, verbose, starttls):
+ dsargs = {
+ SER_LDAP_URL: ldapurl,
+ SER_ROOT_DN: binddn
+ }
+ ds = DirSrv(verbose=verbose)
+ ds.allocate(dsargs)
+ if not ds.can_autobind() and binddn is not None:
+ dsargs[SER_ROOT_PW] = getpass("Enter password for %s on %s : " %
(binddn, ldapurl))
+ elif binddn is None:
+ raise Exception("Must provide a binddn to connect with")
+ ds.allocate(dsargs)
+ ds.open(starttls=starttls)
+ print("")
+ return ds
+
+def disconnect_instance(inst):
+ if inst is not None:
+ inst.close()
+
+def _generic_list(inst, basedn, log, manager_class, **kwargs):
+ mc = manager_class(inst, basedn)
+ ol = mc.list()
+ if len(ol) == 0:
+ print("No objects to display")
+ elif len(ol) > 0:
+ # We might sort this in the future
+ for o in ol:
+ o_str = o.__unicode__()
+ print(o_str)
+
+# Display these entries better!
+def _generic_get(inst, basedn, log, manager_class, selector):
+ mc = manager_class(inst, basedn)
+ o = mc.get(selector)
+ o_str = o.__unicode__()
+ print(o_str)
+
+def _generic_get_dn(inst, basedn, log, manager_class, dn):
+ mc = manager_class(inst, basedn)
+ o = mc.get(dn=dn)
+ o_str = o.__unicode__()
+ print(o_str)
+
+def _generic_create(inst, basedn, log, manager_class, kwargs):
+ mc = manager_class(inst, basedn)
+ o = mc.create(properties=kwargs)
+ o_str = o.__unicode__()
+ print('Sucessfully created %s' % o_str)
+
+def _generic_delete(inst, basedn, log, object_class, dn):
+ # Load the oc direct
+ o = object_class(inst, dn)
+ o.delete()
+ print('Sucessfully deleted %s' % dn)
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
diff --git a/src/lib389/lib389/cli_idm/account.py b/src/lib389/lib389/cli_idm/account.py
new file mode 100644
index 0000000..c98bd23
--- /dev/null
+++ b/src/lib389/lib389/cli_idm/account.py
@@ -0,0 +1,64 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017, Red Hat inc,
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import argparse
+
+from lib389.idm.account import Account, Accounts
+from lib389.cli_base import (
+ _generic_list,
+ _get_arg,
+ )
+
+MANY = Accounts
+
+def list(inst, basedn, log, args):
+ _generic_list(inst, basedn, log.getChild('_generic_list'), MANY)
+
+def status(inst, basedn, log, args):
+ dn = _get_arg( args.dn, msg="Enter dn to check")
+ accounts = Accounts(inst, basedn)
+ acct = accounts.get(dn=dn)
+ acct_str = "locked: %s" % acct.is_locked()
+ log.info('dn: %s' % dn)
+ log.info(acct_str)
+
+def lock(inst, basedn, log, args):
+ dn = _get_arg( args.dn, msg="Enter dn to check")
+ accounts = Accounts(inst, basedn)
+ acct = accounts.get(dn=dn)
+ acct.lock()
+ log.info('locked %s' % dn)
+
+def unlock(inst, basedn, log, args):
+ dn = _get_arg( args.dn, msg="Enter dn to check")
+ accounts = Accounts(inst, basedn)
+ acct = accounts.get(dn=dn)
+ acct.unlock()
+ log.info('unlocked %s' % dn)
+
+
+def create_parser(subparsers):
+ account_parser = subparsers.add_parser('account', help='Manage generic
accounts IE account locking and unlocking.')
+
+ subcommands = account_parser.add_subparsers(help='action')
+
+ list_parser = subcommands.add_parser('list', help='list')
+ list_parser.set_defaults(func=list)
+
+ lock_parser = subcommands.add_parser('lock', help='lock')
+ lock_parser.set_defaults(func=lock)
+ lock_parser.add_argument('dn', nargs='?', help='The dn to
lock')
+
+ status_parser = subcommands.add_parser('status', help='status')
+ status_parser.set_defaults(func=status)
+ status_parser.add_argument('dn', nargs='?', help='The dn to
check')
+
+ unlock_parser = subcommands.add_parser('unlock', help='unlock')
+ unlock_parser.set_defaults(func=unlock)
+ unlock_parser.add_argument('dn', nargs='?', help='The dn to
unlock')
+
diff --git a/src/lib389/lib389/cli_idm/group.py b/src/lib389/lib389/cli_idm/group.py
new file mode 100644
index 0000000..a64eb86
--- /dev/null
+++ b/src/lib389/lib389/cli_idm/group.py
@@ -0,0 +1,76 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import argparse
+from lib389.idm.group import Group, Groups, MUST_ATTRIBUTES
+
+from lib389.cli_base import (
+ populate_attr_arguments,
+ _generic_list,
+ _generic_get,
+ _generic_get_dn,
+ _generic_create,
+ _generic_delete,
+ _get_arg,
+ _get_args,
+ _get_attributes,
+ _warn,
+ )
+
+SINGULAR = Group
+MANY = Groups
+RDN = 'cn'
+
+# These are a generic specification, try not to tamper with them
+
+def list(inst, basedn, log, args):
+ _generic_list(inst, basedn, log.getChild('_generic_list'), MANY)
+
+def get(inst, basedn, log, args):
+ rdn = _get_arg( args.selector, msg="Enter %s to retrieve" % RDN)
+ _generic_get(inst, basedn, log.getChild('_generic_get'), MANY, rdn)
+
+def get_dn(inst, basedn, log, args):
+ dn = lambda args: _get_arg( args.dn, msg="Enter dn to retrieve")
+ _generic_get_dn(inst, basedn, log.getChild('_generic_get_dn'), MANY, dn)
+
+def create(inst, basedn, log, args):
+ kwargs = _get_attributes(args, MUST_ATTRIBUTES)
+ _generic_create(inst, basedn, log.getChild('_generic_create'), MANY, kwargs)
+
+def delete(inst, basedn, log, args):
+ dn = _get_arg( args, msg="Enter dn to delete")
+ _warn(dn, msg="Deleting %s %s" % (SINGULAR.__name__, dn))
+ _generic_delete(inst, basedn, log.getChild('_generic_delete'), SINGULAR, dn)
+
+def create_parser(subparsers):
+ group_parser = subparsers.add_parser('group', help='Manage groups')
+
+ subcommands = group_parser.add_subparsers(help='action')
+
+ list_parser = subcommands.add_parser('list', help='list')
+ list_parser.set_defaults(func=list)
+
+ get_parser = subcommands.add_parser('get', help='get')
+ get_parser.set_defaults(func=get)
+ get_parser.add_argument('selector', nargs='?', help='The term to
search for')
+
+ get_dn_parser = subcommands.add_parser('get_dn', help='get_dn')
+ get_dn_parser.set_defaults(func=get_dn)
+ get_dn_parser.add_argument('dn', nargs='?', help='The dn to
get')
+
+ create_parser = subcommands.add_parser('create', help='create')
+ create_parser.set_defaults(func=create)
+ populate_attr_arguments(create_parser, MUST_ATTRIBUTES)
+
+ delete_parser = subcommands.add_parser('delete', help='deletes the
object')
+ delete_parser.set_defaults(func=delete)
+ delete_parser.add_argument('dn', nargs='?', help='The dn to
delete')
+
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
diff --git a/src/lib389/lib389/cli_idm/initialise.py
b/src/lib389/lib389/cli_idm/initialise.py
new file mode 100644
index 0000000..b9240d2
--- /dev/null
+++ b/src/lib389/lib389/cli_idm/initialise.py
@@ -0,0 +1,21 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389._constants import INSTALL_LATEST_CONFIG
+from lib389.configurations import get_sample_entries
+
+def initialise(inst, basedn, log, args):
+ sample_entries = get_sample_entries(args.version)
+ s_ent = sample_entries(inst, basedn)
+ s_ent.apply()
+
+def create_parser(subparsers):
+ initialise_parser = subparsers.add_parser('initialise', help="Initialise
a backend with domain information and sample entries")
+ initialise_parser.set_defaults(func=initialise)
+ initialise_parser.add_argument('--version', help="The version of entries
to create.", default=INSTALL_LATEST_CONFIG)
+
diff --git a/src/lib389/lib389/cli_idm/organisationalunit.py
b/src/lib389/lib389/cli_idm/organisationalunit.py
new file mode 100644
index 0000000..a9a9a08
--- /dev/null
+++ b/src/lib389/lib389/cli_idm/organisationalunit.py
@@ -0,0 +1,76 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import argparse
+from lib389.idm.organisationalunit import OrganisationalUnit, OrganisationalUnits,
MUST_ATTRIBUTES
+
+from lib389.cli_base import (
+ populate_attr_arguments,
+ _generic_list,
+ _generic_get,
+ _generic_get_dn,
+ _generic_create,
+ _generic_delete,
+ _get_arg,
+ _get_args,
+ _get_attributes,
+ _warn,
+ )
+
+SINGULAR = OrganisationalUnit
+MANY = OrganisationalUnits
+RDN = 'ou'
+
+# These are a generic specification, try not to tamper with them
+
+def list(inst, basedn, log, args):
+ _generic_list(inst, basedn, log.getChild('_generic_list'), MANY)
+
+def get(inst, basedn, log, args):
+ rdn = _get_arg( args.selector, msg="Enter %s to retrieve" % RDN)
+ _generic_get(inst, basedn, log.getChild('_generic_get'), MANY, rdn)
+
+def get_dn(inst, basedn, log, args):
+ dn = lambda args: _get_arg( args.dn, msg="Enter dn to retrieve")
+ _generic_get_dn(inst, basedn, log.getChild('_generic_get_dn'), MANY, dn)
+
+def create(inst, basedn, log, args):
+ kwargs = _get_attributes(args, MUST_ATTRIBUTES)
+ _generic_create(inst, basedn, log.getChild('_generic_create'), MANY, kwargs)
+
+def delete(inst, basedn, log, args):
+ dn = _get_arg( args, msg="Enter dn to delete")
+ _warn(dn, msg="Deleting %s %s" % (SINGULAR.__name__, dn))
+ _generic_delete(inst, basedn, log.getChild('_generic_delete'), SINGULAR, dn)
+
+def create_parser(subparsers):
+ ou_parser = subparsers.add_parser('organisationalunit', help='Manage
organisational units')
+
+ subcommands = ou_parser.add_subparsers(help='action')
+
+ list_parser = subcommands.add_parser('list', help='list')
+ list_parser.set_defaults(func=list)
+
+ get_parser = subcommands.add_parser('get', help='get')
+ get_parser.set_defaults(func=get)
+ get_parser.add_argument('selector', nargs='?', help='The term to
search for')
+
+ get_dn_parser = subcommands.add_parser('get_dn', help='get_dn')
+ get_dn_parser.set_defaults(func=get_dn)
+ get_dn_parser.add_argument('dn', nargs='?', help='The dn to
get')
+
+ create_parser = subcommands.add_parser('create', help='create')
+ create_parser.set_defaults(func=create)
+ populate_attr_arguments(create_parser, MUST_ATTRIBUTES)
+
+ delete_parser = subcommands.add_parser('delete', help='deletes the
object')
+ delete_parser.set_defaults(func=delete)
+ delete_parser.add_argument('dn', nargs='?', help='The dn to
delete')
+
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
diff --git a/src/lib389/lib389/cli_idm/posixgroup.py
b/src/lib389/lib389/cli_idm/posixgroup.py
new file mode 100644
index 0000000..abf6d53
--- /dev/null
+++ b/src/lib389/lib389/cli_idm/posixgroup.py
@@ -0,0 +1,77 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import argparse
+from lib389.idm.posixgroup import PosixGroup, PosixGroups, MUST_ATTRIBUTES
+
+from lib389.cli_base import (
+ populate_attr_arguments,
+ _generic_list,
+ _generic_get,
+ _generic_get_dn,
+ _generic_create,
+ _generic_delete,
+ _get_arg,
+ _get_args,
+ _get_attributes,
+ _warn,
+ )
+
+
+SINGULAR = PosixGroup
+MANY = PosixGroups
+RDN = 'cn'
+
+# These are a generic specification, try not to tamper with them
+
+def list(inst, basedn, log, args):
+ _generic_list(inst, basedn, log.getChild('_generic_list'), MANY)
+
+def get(inst, basedn, log, args):
+ rdn = _get_arg( args.selector, msg="Enter %s to retrieve" % RDN)
+ _generic_get(inst, basedn, log.getChild('_generic_get'), MANY, rdn)
+
+def get_dn(inst, basedn, log, args):
+ dn = lambda args: _get_arg( args.dn, msg="Enter dn to retrieve")
+ _generic_get_dn(inst, basedn, log.getChild('_generic_get_dn'), MANY, dn)
+
+def create(inst, basedn, log, args):
+ kwargs = _get_attributes(args.extra, MUST_ATTRIBUTES)
+ _generic_create(inst, basedn, log.getChild('_generic_create'), MANY, kwargs)
+
+def delete(inst, basedn, log, args):
+ dn = _get_arg( args, msg="Enter dn to delete")
+ _warn(dn, msg="Deleting %s %s" % (SINGULAR.__name__, dn))
+ _generic_delete(inst, basedn, log.getChild('_generic_delete'), SINGULAR, dn)
+
+def create_parser(subparsers):
+ posixgroup_parser = subparsers.add_parser('posixgroup', help='Manage
posix groups')
+
+ subcommands = posixgroup_parser.add_subparsers(help='action')
+
+ list_parser = subcommands.add_parser('list', help='list')
+ list_parser.set_defaults(func=list)
+
+ get_parser = subcommands.add_parser('get', help='get')
+ get_parser.set_defaults(func=get)
+ get_parser.add_argument('selector', nargs='?', help='The term to
search for')
+
+ get_dn_parser = subcommands.add_parser('get_dn', help='get_dn')
+ get_dn_parser.set_defaults(func=get_dn)
+ get_dn_parser.add_argument('dn', nargs='?', help='The dn to
get')
+
+ create_parser = subcommands.add_parser('create', help='create')
+ create_parser.set_defaults(func=create)
+ populate_attr_arguments(create_parser, MUST_ATTRIBUTES)
+
+ delete_parser = subcommands.add_parser('delete', help='deletes the
object')
+ delete_parser.set_defaults(func=delete)
+ delete_parser.add_argument('dn', nargs='?', help='The dn to
delete')
+
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
diff --git a/src/lib389/lib389/cli_idm/user.py b/src/lib389/lib389/cli_idm/user.py
new file mode 100644
index 0000000..5b57862
--- /dev/null
+++ b/src/lib389/lib389/cli_idm/user.py
@@ -0,0 +1,76 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import argparse
+from lib389.idm.user import UserAccount, UserAccounts, MUST_ATTRIBUTES
+
+from lib389.cli_base import (
+ populate_attr_arguments,
+ _generic_list,
+ _generic_get,
+ _generic_get_dn,
+ _generic_create,
+ _generic_delete,
+ _get_arg,
+ _get_args,
+ _get_attributes,
+ _warn,
+ )
+
+SINGULAR = UserAccount
+MANY = UserAccounts
+RDN = 'uid'
+
+# These are a generic specification, try not to tamper with them
+
+def list(inst, basedn, log, args):
+ _generic_list(inst, basedn, log.getChild('_generic_list'), MANY)
+
+def get(inst, basedn, log, args):
+ rdn = _get_arg( args.selector, msg="Enter %s to retrieve" % RDN)
+ _generic_get(inst, basedn, log.getChild('_generic_get'), MANY, rdn)
+
+def get_dn(inst, basedn, log, args):
+ dn = lambda args: _get_arg( args.dn, msg="Enter dn to retrieve")
+ _generic_get_dn(inst, basedn, log.getChild('_generic_get_dn'), MANY, dn)
+
+def create(inst, basedn, log, args):
+ kwargs = _get_attributes(args, MUST_ATTRIBUTES)
+ _generic_create(inst, basedn, log.getChild('_generic_create'), MANY, kwargs)
+
+def delete(inst, basedn, log, args):
+ dn = _get_arg( args, msg="Enter dn to delete")
+ _warn(dn, msg="Deleting %s %s" % (SINGULAR.__name__, dn))
+ _generic_delete(inst, basedn, log.getChild('_generic_delete'), SINGULAR, dn)
+
+def create_parser(subparsers):
+ user_parser = subparsers.add_parser('user', help='Manage posix
users')
+
+ subcommands = user_parser.add_subparsers(help='action')
+
+ list_parser = subcommands.add_parser('list', help='list')
+ list_parser.set_defaults(func=list)
+
+ get_parser = subcommands.add_parser('get', help='get')
+ get_parser.set_defaults(func=get)
+ get_parser.add_argument('selector', nargs='?', help='The term to
search for')
+
+ get_dn_parser = subcommands.add_parser('get_dn', help='get_dn')
+ get_dn_parser.set_defaults(func=get_dn)
+ get_dn_parser.add_argument('dn', nargs='?', help='The dn to
get')
+
+ create_parser = subcommands.add_parser('create', help='create')
+ create_parser.set_defaults(func=create)
+ populate_attr_arguments(create_parser, MUST_ATTRIBUTES)
+
+ delete_parser = subcommands.add_parser('delete', help='deletes the
object')
+ delete_parser.set_defaults(func=delete)
+ delete_parser.add_argument('dn', nargs='?', help='The dn to
delete')
+
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
diff --git a/src/lib389/lib389/clitools/__init__.py
b/src/lib389/lib389/clitools/__init__.py
new file mode 100755
index 0000000..cd66ce1
--- /dev/null
+++ b/src/lib389/lib389/clitools/__init__.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# Probably need some helpers for argparse and instance selection
+
+from argparse import ArgumentParser
+from getpass import getpass
+from lib389 import DirSrv
+from lib389._constants import *
+
+
+class CliTool(object):
+ def __init__(self, args=None):
+ if args is not None:
+ self.args = args
+ self.ds = DirSrv(verbose=args.verbose)
+ else:
+ self.ds = DirSrv()
+
+ def populate_instance_dict(self, instance):
+ insts = self.ds.list(serverid=instance)
+ if len(insts) != 1:
+ # Raise an exception here?
+ self.inst = None
+ raise ValueError("No such instance %s" % instance)
+ else:
+ self.inst = insts[0]
+
+ def get_rootdn_pass(self):
+ if self.args.binddn is None:
+ binddn = self.inst[SER_ROOT_DN]
+ else:
+ binddn = self.args.binddn
+ # There is a dict get key thing somewhere ...
+ if self.inst.get(SER_ROOT_PW, None) is None:
+ prompt_txt = ('Enter password for %s on instance %s: ' %
+ (binddn,
+ self.inst[SER_SERVERID_PROP]))
+ self.inst[SER_ROOT_PW] = getpass(prompt_txt)
+ print("")
+ return
+
+ def connect(self):
+ # Can we attempt the autobind?
+ # This should be a bit cleaner perhaps
+ # Perhaps an argument to the cli?
+ self.ds.allocate(self.inst)
+ if not self.ds.can_autobind():
+ self.get_rootdn_pass()
+ self.ds.allocate(self.inst)
+ self.ds.open()
+
+ def disconnect(self):
+ # Is there a ds unbind / disconnect?
+ self.ds.close()
+
+
+def _clitools_parser():
+ parser = ArgumentParser()
+ parser.add_argument('-Z', '--instance',
+ help='The name of the DS instance to connect to and ' +
+ 'work upon.', required=True)
+ parser.add_argument('-D', '--binddn',
+ help='The bind dn to use for operations. Defaults to ' +
+ 'rooddn', default=None)
+ parser.add_argument('-v', '--verbose', help="Display verbose
debug information", action='store_true', default=False)
+ return parser
+
+clitools_parser = _clitools_parser()
+
+if __name__ == '__main__':
+ args = clitools_parser.parse_args()
diff --git a/src/lib389/lib389/clitools/ds_aci_lint
b/src/lib389/lib389/clitools/ds_aci_lint
new file mode 100755
index 0000000..c967cdb
--- /dev/null
+++ b/src/lib389/lib389/clitools/ds_aci_lint
@@ -0,0 +1,41 @@
+#!/usr/bin/python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# from clitools import clitools_parser, get_instance_dict, get_rootdn_pass
+from lib389.clitools import CliTool, clitools_parser
+# from lib389 import DirSrv
+from lib389._constants import *
+from argparse import ArgumentParser
+
+
+class AciLintTool(CliTool):
+ def aci_lint(self):
+ try:
+ self.populate_instance_dict(self.args.instance)
+ self.connect()
+ # This is pretty rough, it just dumps the objects
+ (result, details) = status = self.ds.aci.lint(self.args.suffix)
+ print(self.ds.aci.format_lint(details))
+ if result:
+ print("PASS")
+ else:
+ print("FAIL")
+ finally:
+ self.disconnect()
+
+if __name__ == '__main__':
+ # Do some arg parse stuff
+ # You can always add a child parser here too ...
+ parser = clitools_parser.add_argument_group('aci', 'aci linting
options')
+ parser.add_argument('-b', '--suffix', help='The name of the
suffix to ' +
+ ' validate acis.', required=True)
+ args = clitools_parser.parse_args()
+ acilinttool = AciLintTool(args)
+ acilinttool.aci_lint()
diff --git a/src/lib389/lib389/clitools/ds_backend_getattr
b/src/lib389/lib389/clitools/ds_backend_getattr
new file mode 100755
index 0000000..cb8928b
--- /dev/null
+++ b/src/lib389/lib389/clitools/ds_backend_getattr
@@ -0,0 +1,42 @@
+#!/usr/bin/python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# from clitools import clitools_parser, get_instance_dict, get_rootdn_pass
+from lib389.clitools import CliTool, clitools_parser
+# from lib389 import DirSrv
+from lib389._constants import *
+from argparse import ArgumentParser
+
+from lib389.backend import Backends
+
+class BackendTool(CliTool):
+ def backend_backend_list(self):
+ try:
+ self.populate_instance_dict(self.args.instance)
+ self.connect()
+ # This is pretty rough, it just dumps the objects
+ be = Backends(instance=self.ds).get(self.args.backend)
+ for attr in self.args.attributes:
+ print("%s : %s" % (attr, be.get(attr) ))
+
+ finally:
+ self.disconnect()
+
+if __name__ == '__main__':
+ # Do some arg parse stuff
+ # You can always add a child parser here too ...
+ parser = clitools_parser.add_argument_group('monitor', 'monitoring
options')
+ parser.add_argument('-n', '--backend', help='The name of the
backend to ' +
+ ' retrieve monitoring information for.', required=True)
+ parser.add_argument('attributes', metavar='attr', type=str,
nargs="+",
+ help='A list of attributes to retrieve')
+ args = clitools_parser.parse_args()
+ backendtool = BackendTool(args)
+ backendtool.backend_backend_list()
diff --git a/src/lib389/lib389/clitools/ds_backend_list
b/src/lib389/lib389/clitools/ds_backend_list
new file mode 100755
index 0000000..67c8b31
--- /dev/null
+++ b/src/lib389/lib389/clitools/ds_backend_list
@@ -0,0 +1,39 @@
+#!/usr/bin/python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# from clitools import clitools_parser, get_instance_dict, get_rootdn_pass
+from lib389.clitools import CliTool, clitools_parser
+# from lib389 import DirSrv
+from lib389._constants import *
+from argparse import ArgumentParser
+
+from lib389.backend import Backends
+
+
+class BackendTool(CliTool):
+ def backend_backend_list(self):
+ try:
+ self.populate_instance_dict(self.args.instance)
+ self.connect()
+ # This is pretty rough, it just dumps the objects
+ backends = Backends(instance=self.ds)
+ for be in backends.list():
+ print("backend: %s" % be)
+
+ finally:
+ self.disconnect()
+
+if __name__ == '__main__':
+ # Do some arg parse stuff
+ # You can always add a child parser here too ...
+ parser = clitools_parser.add_argument_group('monitor', 'monitoring
options')
+ args = clitools_parser.parse_args()
+ backendtool = BackendTool(args)
+ backendtool.backend_backend_list()
diff --git a/src/lib389/lib389/clitools/ds_backend_setattr
b/src/lib389/lib389/clitools/ds_backend_setattr
new file mode 100755
index 0000000..5706ed3
--- /dev/null
+++ b/src/lib389/lib389/clitools/ds_backend_setattr
@@ -0,0 +1,43 @@
+#!/usr/bin/python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# from clitools import clitools_parser, get_instance_dict, get_rootdn_pass
+from lib389.clitools import CliTool, clitools_parser
+# from lib389 import DirSrv
+from lib389._constants import *
+from argparse import ArgumentParser
+
+
+class BackendTool(CliTool):
+ def backend_set_attr(self):
+ try:
+ self.populate_instance_dict(self.args.instance)
+ self.connect()
+ # This is pretty rough, it just dumps the objects
+ props = self.ds.backend.setProperties(bename=self.args.backend,
prop=self.args.attribute[0], values=self.args.values)
+ print(props)
+
+ finally:
+ self.disconnect()
+
+if __name__ == '__main__':
+ # Do some arg parse stuff
+ # You can always add a child parser here too ...
+ parser = clitools_parser.add_argument_group('monitor', 'monitoring
options')
+ parser.add_argument('-n', '--backend', help='The name of the
backend to ' +
+ ' retrieve monitoring information for.', required=True)
+ parser.add_argument('attribute', metavar='attr', type=str, nargs=1,
+ help='The attribute we wish to change')
+ parser.add_argument('values', metavar='values', type=str,
nargs='+',
+ help='The values to apply to the attribute')
+
+ args = clitools_parser.parse_args()
+ backendtool = BackendTool(args)
+ backendtool.backend_set_attr()
diff --git a/src/lib389/lib389/clitools/ds_krb_create_keytab
b/src/lib389/lib389/clitools/ds_krb_create_keytab
new file mode 100755
index 0000000..11c345a
--- /dev/null
+++ b/src/lib389/lib389/clitools/ds_krb_create_keytab
@@ -0,0 +1,40 @@
+#!/usr/bin/python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389.clitools import CliTool, clitools_parser
+from lib389._constants import *
+from lib389.mit_krb5 import MitKrb5
+from argparse import ArgumentParser
+
+
+class MitKrb5Tool(CliTool):
+ def mit_krb5_realm_create(self):
+ try:
+ krb = MitKrb5(realm=args.realm, warnings=True)
+ krb.create_keytab(principal=args.principal, keytab=args.keytab)
+ finally:
+ pass
+
+if __name__ == '__main__':
+ # Do some arg parse stuff
+ # You can always add a child parser here too ...
+ parser = clitools_parser.add_argument_group('krb', 'kerberos
options')
+ parser.add_argument('--realm', '-r',
+ help='The name of the realm to create',
+ required=True)
+ parser.add_argument('--principal', '-p',
+ help='The name of the principal to create',
+ required=True)
+ parser.add_argument('--keytab', '-k',
+ help='The path to the keytab to create',
+ required=True)
+ args = clitools_parser.parse_args()
+ mittool = MitKrb5Tool(args)
+ mittool.mit_krb5_realm_create()
diff --git a/src/lib389/lib389/clitools/ds_krb_create_principal
b/src/lib389/lib389/clitools/ds_krb_create_principal
new file mode 100755
index 0000000..f4b59b2
--- /dev/null
+++ b/src/lib389/lib389/clitools/ds_krb_create_principal
@@ -0,0 +1,38 @@
+#!/usr/bin/python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+
+from lib389.clitools import CliTool, clitools_parser
+from lib389._constants import *
+from lib389.mit_krb5 import MitKrb5
+from argparse import ArgumentParser
+
+
+class MitKrb5Tool(CliTool):
+ def mit_krb5_realm_create(self):
+ try:
+ krb = MitKrb5(realm=args.realm, warnings=True)
+ krb.create_principal(principal=args.principal)
+ finally:
+ pass
+
+if __name__ == '__main__':
+ # Do some arg parse stuff
+ # You can always add a child parser here too ...
+ parser = clitools_parser.add_argument_group('krb', 'kerberos
options')
+ parser.add_argument('--realm', '-r',
+ help='The name of the realm to create',
+ required=True)
+ parser.add_argument('--principal', '-p',
+ help='The name of the principal to create',
+ required=True)
+ args = clitools_parser.parse_args()
+ mittool = MitKrb5Tool(args)
+ mittool.mit_krb5_realm_create()
diff --git a/src/lib389/lib389/clitools/ds_krb_create_realm
b/src/lib389/lib389/clitools/ds_krb_create_realm
new file mode 100755
index 0000000..23aafc4
--- /dev/null
+++ b/src/lib389/lib389/clitools/ds_krb_create_realm
@@ -0,0 +1,34 @@
+#!/usr/bin/python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389.clitools import CliTool, clitools_parser
+from lib389._constants import *
+from lib389.mit_krb5 import MitKrb5
+from argparse import ArgumentParser
+
+
+class MitKrb5Tool(CliTool):
+ def mit_krb5_realm_create(self):
+ try:
+ krb = MitKrb5(realm=args.realm, warnings=True)
+ krb.create_realm()
+ finally:
+ pass
+
+if __name__ == '__main__':
+ # Do some arg parse stuff
+ # You can always add a child parser here too ...
+ parser = clitools_parser.add_argument_group('krb', 'kerberos
options')
+ parser.add_argument('--realm', '-r',
+ help='The name of the realm to create',
+ required=True)
+ args = clitools_parser.parse_args()
+ mittool = MitKrb5Tool(args)
+ mittool.mit_krb5_realm_create()
diff --git a/src/lib389/lib389/clitools/ds_krb_destroy_realm
b/src/lib389/lib389/clitools/ds_krb_destroy_realm
new file mode 100755
index 0000000..ce2938f
--- /dev/null
+++ b/src/lib389/lib389/clitools/ds_krb_destroy_realm
@@ -0,0 +1,34 @@
+#!/usr/bin/python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389.clitools import CliTool, clitools_parser
+from lib389._constants import *
+from lib389.mit_krb5 import MitKrb5
+from argparse import ArgumentParser
+
+
+class MitKrb5Tool(CliTool):
+ def mit_krb5_realm_destroy(self):
+ try:
+ krb = MitKrb5(realm=args.realm, warnings=True)
+ krb.destroy_realm()
+ finally:
+ pass
+
+if __name__ == '__main__':
+ # Do some arg parse stuff
+ # You can always add a child parser here too ...
+ parser = clitools_parser.add_argument_group('krb', 'kerberos
options')
+ parser.add_argument('--realm', '-r',
+ help='The name of the realm to DESTROY',
+ required=True)
+ args = clitools_parser.parse_args()
+ mittool = MitKrb5Tool(args)
+ mittool.mit_krb5_realm_destroy()
diff --git a/src/lib389/lib389/clitools/ds_monitor_backend
b/src/lib389/lib389/clitools/ds_monitor_backend
new file mode 100755
index 0000000..02d09b3
--- /dev/null
+++ b/src/lib389/lib389/clitools/ds_monitor_backend
@@ -0,0 +1,39 @@
+#!/usr/bin/python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# from clitools import clitools_parser, get_instance_dict, get_rootdn_pass
+from lib389.clitools import CliTool, clitools_parser
+# from lib389 import DirSrv
+from lib389._constants import *
+from argparse import ArgumentParser
+
+
+class MonitorTool(CliTool):
+ def monitor_backend_list(self):
+ try:
+ self.populate_instance_dict(self.args.instance)
+ self.connect()
+ # This is pretty rough, it just dumps the objects
+ status = self.ds.monitor.backend(self.args.backend)
+ print ("dn: %s" % status.pop('dn'))
+ for monitor in status:
+ print("%s: %s" % (monitor, status[monitor][0]))
+ finally:
+ self.disconnect()
+
+if __name__ == '__main__':
+ # Do some arg parse stuff
+ # You can always add a child parser here too ...
+ parser = clitools_parser.add_argument_group('monitor', 'monitoring
options')
+ parser.add_argument('-n', '--backend', help='The name of the
backend to ' +
+ ' retrieve monitoring information for.', required=True)
+ args = clitools_parser.parse_args()
+ monitortool = MonitorTool(args)
+ monitortool.monitor_backend_list()
diff --git a/src/lib389/lib389/clitools/ds_monitor_server
b/src/lib389/lib389/clitools/ds_monitor_server
new file mode 100755
index 0000000..0f574cd
--- /dev/null
+++ b/src/lib389/lib389/clitools/ds_monitor_server
@@ -0,0 +1,35 @@
+#!/usr/bin/python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# from clitools import clitools_parser, get_instance_dict, get_rootdn_pass
+from lib389.clitools import CliTool, clitools_parser
+# from lib389 import DirSrv
+from lib389._constants import *
+
+
+class MonitorTool(CliTool):
+ def monitor_server_list(self):
+ try:
+ self.populate_instance_dict(self.args.instance)
+ self.connect()
+ # This is pretty rough, it just dumps the objects
+ status = self.ds.monitor.server()
+ print ("dn: %s" % status.pop('dn'))
+ for monitor in status:
+ print("%s: %s" % (monitor, status[monitor][0]))
+ finally:
+ self.disconnect()
+
+if __name__ == '__main__':
+ # Do some arg parse stuff
+ # You can always add a child parser here too ...
+ args = clitools_parser.parse_args()
+ monitortool = MonitorTool(args)
+ monitortool.monitor_server_list()
diff --git a/src/lib389/lib389/clitools/ds_schema_attributetype_list
b/src/lib389/lib389/clitools/ds_schema_attributetype_list
new file mode 100755
index 0000000..f73e331
--- /dev/null
+++ b/src/lib389/lib389/clitools/ds_schema_attributetype_list
@@ -0,0 +1,33 @@
+#!/usr/bin/python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# from clitools import clitools_parser, get_instance_dict, get_rootdn_pass
+from lib389.clitools import CliTool, clitools_parser
+# from lib389 import DirSrv
+from lib389._constants import *
+import ldap
+
+
+class SchemaTool(CliTool):
+ def schema_attributetype_list(self):
+ try:
+ self.populate_instance_dict(self.args.instance)
+ self.connect()
+ for attributetype in self.ds.schema.get_attributetypes():
+ print(attributetype)
+ finally:
+ self.disconnect()
+
+if __name__ == '__main__':
+ # Do some arg parse stuff
+ # You can always add a child parser here too ...
+ args = clitools_parser.parse_args()
+ schematool = SchemaTool(args)
+ schematool.schema_attributetype_list()
diff --git a/src/lib389/lib389/clitools/ds_schema_attributetype_query
b/src/lib389/lib389/clitools/ds_schema_attributetype_query
new file mode 100755
index 0000000..13ea479
--- /dev/null
+++ b/src/lib389/lib389/clitools/ds_schema_attributetype_query
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389.clitools import CliTool, clitools_parser
+from lib389._constants import *
+import ldap
+from argparse import ArgumentParser
+
+
+class SchemaTool(CliTool):
+ def schema_attributetype_query(self):
+ try:
+ self.populate_instance_dict(self.args.instance)
+ self.connect()
+ attributetype, must, may = \
+ self.ds.schema.query_attributetype(self.args.attributetype)
+ print(attributetype)
+ print("")
+ print('MUST')
+ for objectclass in must:
+ print(objectclass)
+ print("")
+ print('MAY')
+ for objectclass in may:
+ print(objectclass)
+ finally:
+ self.disconnect()
+
+if __name__ == '__main__':
+ # Do some arg parse stuff
+ # You can always add a child parser here too ...
+ parser = clitools_parser.add_argument_group('schema', 'schema
options')
+ parser.add_argument('--attributetype',
+ '-a',
+ help='The name of the attribute type to query',
+ required=True)
+ args = clitools_parser.parse_args()
+ schematool = SchemaTool(args)
+ schematool.schema_attributetype_query()
diff --git a/src/lib389/lib389/clitools/ds_setup b/src/lib389/lib389/clitools/ds_setup
new file mode 100755
index 0000000..212a34a
--- /dev/null
+++ b/src/lib389/lib389/clitools/ds_setup
@@ -0,0 +1,78 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+## William Brown <wibrown(a)redhat.com> 2016
+
+from lib389.tools import SetupDs
+from argparse import ArgumentParser
+from getpass import getpass
+import os
+import time
+import sys
+
+def ds_setup_main():
+ """
+ Get and extract the information. Pass it to the setupds tool.
+ """
+ # Check the CLI arguments.
+ parser = ArgumentParser()
+ parser.add_argument('-v', '--verbose',
+ help="Display extra debugging information during
setup", action='store_true')
+ parser.add_argument('-n', '--dryrun',
+ help="Dryrun: Validate system and configurations only",
action='store_true')
+ parser.add_argument('-f', '--file',
+ help="Inf file to use with out user
interaction",required=True)
+ parser.add_argument('--IsolemnlyswearthatIamuptonogood',
dest="ack",
+ help="""You are here likely here by mistake! You
want setup-ds.pl!
+By setting this value you acknowledge and take responsibility for the fact this command
is UNTESTED and NOT READY. You are ON YOUR OWN!
+""",
+ action='store_true')
+
+ args = parser.parse_args()
+ # Create the setupDs object
+ if not args.ack:
+ sys.exit(0)
+ else:
+ print("""
+ _________________________________________
+/ This is not what you want! Press ctrl-c \\
+\ now ... /
+ -----------------------------------------
+ \\ / \\ //\\
+ \\ |\\___/| / \\// \\\\
+ /0 0 \\__ / // | \\ \\
+ / / \\/_/ // | \\ \\
+ @_^_@'/ \\/_ // | \\ \\
+ //_^_/ \\/_ // | \\ \\
+ ( //) | \\/// | \\ \\
+ ( / /) _|_ / ) // | \\ _\\
+ ( // /) '/,_ _ _/ ( ; -. | _ _\\.-~ .-~~~^-.
+ (( / / )) ,-{ _ `-.|.-~-. .~ `.
+ (( // / )) '/\\ / ~-. _ .-~ .-~^-. \\
+ (( /// )) `. { } / \\ \\
+ (( / )) .----~-.\\ \\-' .~ \\ `. \\^-.
+ ///.----..> \\ _ -~ `. ^-` ^-_
+ ///-._ _ _ _ _ _ _}^ - - - - ~ ~-- ,.-~
+ /.-~
+ """)
+ for i in range(1,11):
+ print('%s ...' % (10 - int(i)))
+ time.sleep(1)
+ print('Launching ...')
+ sd = SetupDs(args.verbose, args.dryrun)
+ if sd.create_from_inf(args.file):
+ print("Sucessfully created instance")
+ else:
+ print("Failed to create instance")
+ sys.exit(1)
+ # call create on it.
+ # wait ....
+
+if __name__ == '__main__':
+ ds_setup_main()
+
diff --git a/src/lib389/lib389/config.py b/src/lib389/lib389/config.py
new file mode 100644
index 0000000..0a7539b
--- /dev/null
+++ b/src/lib389/lib389/config.py
@@ -0,0 +1,353 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""Brooker classes to organize ldap methods.
+ Stuff is split in classes, like:
+ * Replica
+ * Backend
+ * Suffix
+
+ You will access this from:
+ DirSrv.backend.methodName()
+"""
+
+import ldap
+from lib389._constants import *
+from lib389 import Entry
+from lib389._mapped_object import DSLdapObject
+from lib389.utils import ensure_bytes, ensure_str
+
+from lib389.lint import DSCLE0001, DSCLE0002, DSELE0001
+
+class Config(DSLdapObject):
+ """
+ Manage "cn=config" tree, including:
+ - enable SSL
+ - set access and error logging
+ - get and set "cn=config" attributes
+ """
+ def __init__(self, conn, batch=False):
+ """@param conn - a DirSrv instance """
+ super(Config, self).__init__(instance=conn, batch=batch)
+ self._dn = DN_CONFIG
+ # self._instance = conn
+ # self.log = conn.log
+ config_compare_exclude = [
+ 'nsslapd-ldapifilepath',
+ 'nsslapd-accesslog',
+ 'nsslapd-auditfaillog',
+ 'nsslapd-ldifdir',
+ 'nsslapd-errorlog',
+ 'nsslapd-instancedir',
+ 'nsslapd-lockdir',
+ 'nsslapd-bakdir',
+ 'nsslapd-schemadir',
+ 'nsslapd-auditlog',
+ 'nsslapd-rootpw',
+ 'nsslapd-workingdir',
+ 'nsslapd-certdir'
+ ]
+ self._compare_exclude = self._compare_exclude + config_compare_exclude
+ self._rdn_attribute = 'cn'
+ self._lint_functions = [self._lint_hr_timestamp, self._lint_passwordscheme]
+
+ @property
+ def dn(self):
+ return DN_CONFIG
+
+ @property
+ def rdn(self):
+ return DN_CONFIG
+
+ def _alter_log_enabled(self, service, state):
+ if service not in ('access', 'error', 'audit'):
+ self._log.error('Attempted to enable invalid log service
"%s"' % service)
+ service = 'nsslapd-%slog-logging-enabled' % service
+ self._log.debug('Setting log %s to %s' % (service, state))
+ self.set(service, state)
+
+ def enable_log(self, service):
+ """Enable a logging service in the 389ds instance.
+ @param service - The logging service to enable. Can be one of 'access',
+ 'error' or 'audit'.
+
+ ex. enable_log('audit')
+ """
+ self._alter_log_enabled(service, 'on')
+
+ def disable_log(self, service):
+ """Disable a logging service in the 389ds instance.
+ @param service - The logging service to Disable. Can be one of 'access'
+ , 'error' or 'audit'.
+
+ ex. disable_log('audit')
+ """
+ self._alter_log_enabled(service, 'off')
+
+ def loglevel(self, vals=(LOG_DEFAULT,), service='error', update=False):
+ """Set the access or error log level.
+ @param vals - a list of log level codes (eg. lib389.LOG_*)
+ defaults to LOG_DEFAULT
+ @param service - 'access' or 'error'. There is no 'audit'
log level.
+ use enable_log or disable_log.
+ @param update - False for replace (default), True for update
+
+ ex. loglevel([lib389.LOG_DEFAULT, lib389.LOG_ENTRY_PARSER])
+ """
+ if service not in ('access', 'error'):
+ self._log.error('Attempted to set level on invalid log service
"%s"' % service)
+ service = 'nsslapd-%slog-level' % service
+ assert len(vals) > 0, "set at least one log level"
+ tot = 0
+ for v in vals:
+ tot |= v
+
+ if update:
+ old = int(self.get(service))
+ tot |= old
+ self._log.debug("Update %s value: %r -> %r" % (service, old,
tot))
+ else:
+ self._log.debug("Replace %s with value: %r" % (service, tot))
+
+ self.set(service, str(tot))
+ return tot
+
+ def logbuffering(self, state=True):
+ if state:
+ value = 'on'
+ else:
+ value = 'off'
+
+ self.set('nsslapd-accesslog-logbuffering', value)
+
+ def reset(self, key):
+ self.set(key, None, action=ldap.MOD_DELETE)
+
+ # THIS WILL BE SPLIT OUT TO ITS OWN MODULE
+ def enable_ssl(self, secport=636, secargs=None):
+ """Configure SSL support into cn=encryption,cn=config.
+
+ secargs is a dict like {
+ 'nsSSLPersonalitySSL': 'Server-Cert'
+ }
+ """
+ self._log.debug("config.enable_ssl is deprecated! Use RSA, Encryption
instead!")
+ self._log.debug("configuring SSL with secargs:%r" % secargs)
+ secargs = secargs or {}
+
+ dn_enc = 'cn=encryption,cn=config'
+ ciphers = ('-rsa_null_md5,+rsa_rc4_128_md5,+rsa_rc4_40_md5,'
+ '+rsa_rc2_40_md5,+rsa_des_sha,+rsa_fips_des_sha,'
+ '+rsa_3des_sha,+rsa_fips_3des_sha,+tls_rsa_export1024'
+ '_with_rc4_56_sha,+tls_rsa_export1024_with_des_cbc_sha')
+ mod = [(ldap.MOD_REPLACE, 'nsSSL3', secargs.get('nsSSL3',
'on')),
+ (ldap.MOD_REPLACE, 'nsSSLClientAuth',
+ secargs.get('nsSSLClientAuth', 'allowed')),
+ (ldap.MOD_REPLACE, 'nsSSL3Ciphers',
secargs.get('nsSSL3Ciphers',
+ ciphers))]
+ self._instance.modify_s(dn_enc, mod)
+
+ dn_rsa = 'cn=RSA,cn=encryption,cn=config'
+ e_rsa = Entry(dn_rsa)
+ e_rsa.update({
+ 'objectclass': ['top', 'nsEncryptionModule'],
+ 'nsSSLPersonalitySSL': secargs.get('nsSSLPersonalitySSL',
+ 'Server-Cert'),
+ 'nsSSLToken': secargs.get('nsSSLToken', 'internal
(software)'),
+ 'nsSSLActivation': secargs.get('nsSSLActivation',
'on')
+ })
+ try:
+ self._instance.add_s(e_rsa)
+ except ldap.ALREADY_EXISTS:
+ pass
+
+ mod = [
+ (ldap.MOD_REPLACE,
+ 'nsslapd-security',
+ secargs.get('nsslapd-security', 'on')),
+ (ldap.MOD_REPLACE,
+ 'nsslapd-ssl-check-hostname',
+ secargs.get('nsslapd-ssl-check-hostname', 'off')),
+ (ldap.MOD_REPLACE,
+ 'nsslapd-secureport',
+ str(secport))
+ ]
+ self._log.debug("trying to modify %r with %r" % (DN_CONFIG, mod))
+ self._instance.modify_s(DN_CONFIG, mod)
+
+ fields = 'nsslapd-security nsslapd-ssl-check-hostname'.split()
+ return self._instance.getEntry(DN_CONFIG, attrlist=fields)
+
+ def _lint_hr_timestamp(self):
+ hr_timestamp =
self.get_attr_val('nsslapd-logging-hr-timestamps-enabled')
+ if ensure_bytes('on') != hr_timestamp:
+ return DSCLE0001
+ pass # nsslapd-logging-hr-timestamps-enabled
+
+ def _lint_passwordscheme(self):
+ allowed_schemes = ['SSHA512', 'PBKDF2_SHA256']
+ u_password_scheme = self.get_attr_val_utf8('passwordStorageScheme')
+ u_root_scheme = self.get_attr_val_utf8('nsslapd-rootpwstoragescheme')
+ if u_root_scheme not in allowed_schemes or u_password_scheme not in
allowed_schemes:
+ return DSCLE0002
+ return None
+
+class Encryption(DSLdapObject):
+ """
+ Manage "cn=encryption,cn=config" tree, including:
+ - ssl ciphers
+ - ssl / tls levels
+ """
+ def __init__(self, conn, batch=False):
+ """@param conn - a DirSrv instance """
+ super(Encryption, self).__init__(instance=conn, batch=batch)
+ self._dn = 'cn=encryption,%s' % DN_CONFIG
+ self._create_objectclasses = ['top', 'nsEncryptionConfig']
+ # Once created, don't allow it's removal
+ self._rdn_attribute = 'cn'
+ self._must_attributes = ['cn']
+ self._protected = True
+ self._lint_functions = [self._lint_check_tls_version]
+
+ def create(self, rdn=None, properties={'cn': 'encryption',
'nsSSLClientAuth': 'allowed'}):
+ if rdn is not None:
+ self._log.debug("dn on cn=encryption is not None. This is a
mistake.")
+ super(Encryption, self).create(properties=properties)
+
+ def _lint_check_tls_version(self):
+ tls_min = self.get_attr_val('sslVersionMin');
+ if tls_min < ensure_bytes('TLS1.1'):
+ return DSELE0001
+ return None
+
+class RSA(DSLdapObject):
+ """
+ Manage the "cn=RSA,cn=encryption,cn=config" object
+ - Set the certificate name
+ - Database path
+ - ssl token name
+ """
+ def __init__(self, conn, batch=False):
+ """@param conn - a DirSrv instance """
+ super(RSA, self).__init__(instance=conn, batch=batch)
+ self._dn = 'cn=RSA,cn=encryption,%s' % DN_CONFIG
+ self._create_objectclasses = ['top', 'nsEncryptionModule']
+ self._rdn_attribute = 'cn'
+ self._must_attributes = ['cn']
+ # Once we create it, don't remove it
+ self._protected = True
+
+ def _validate(self, rdn, properties, basedn):
+ (dn, valid_props) = super(RSA, self)._validate(rdn, properties, basedn)
+ # Ensure that dn matches self._dn
+ assert(self._dn == dn)
+ return (dn, valid_props)
+
+ def create(self, rdn=None, properties={'cn': 'RSA',
'nsSSLPersonalitySSL': 'Server-Cert', 'nsSSLActivation':
'on', 'nsSSLToken': 'internal (software)'}):
+ # Is this the best way to for the dn?
+ if rdn is not None:
+ self._log.debug("dn on cn=Rsa create request is not None. This is a
mistake.")
+ # Our self._dn is already set, no need for rdn.
+ super(RSA, self).create(properties=properties)
+
+class CertmapLegacy(object):
+ """
+ Manage certificate mappings in Directory Server
+
+ This is based on the old certmap fileformat. As a result, the interface is rather
+ crude, until we re-write to use dynamic mappings.
+ """
+ def __init__(self, conn):
+ self._instance = conn
+ pass
+
+ def reset(self):
+ """
+ Reset the certmap to empty.
+ """
+ certmap = os.path.join(self._instance.get_config_dir(), 'certmap.conf')
+ with open(certmap, 'w') as f:
+ f.write('# LDAP Certificate mappings \n')
+
+ def _parse_maps(self, maps):
+ certmaps = {}
+ cur_map = None
+ for l in maps:
+ if l.startswith('certmap'):
+ # Line matches format of: certmap name issuer
+ (_, name, issuer) = l.split(None, 2)
+ certmaps[name] = {
+ 'DNComps': None,
+ 'FilterComps': None,
+ 'VerifyCert': None,
+ 'CmapLdapAttr': None,
+ 'Library': None,
+ 'InitFn': None,
+ }
+ certmaps[name]['issuer'] = issuer
+ else:
+ # The line likely is:
+ # name:property [value]
+ (name, pav) = l.split(':')
+ pavs = pav.split(None, 1)
+ if len(pavs) == 1:
+ # We have an empty property
+ certmaps[name][pav] = ''
+ else:
+ # We clearly have a value.
+ if pavs[0] == 'DNComps' or pavs[0] == 'FilterComps':
+ # These two are comma sep lists
+ values = [w.split for w in pavs[1].split(',')]
+ certmaps[name][pavs[0]] = values
+ else:
+ certmaps[name][pavs[0]] = pavs[1]
+ return certmaps
+
+ def list(self):
+ """
+ Parse and list current certmaps.
+ """
+ certmap = os.path.join(self._instance.get_config_dir(), 'certmap.conf')
+ maps = []
+ with open(certmap, 'r') as f:
+ for line in f.readlines():
+ s_line = line.strip()
+ if not s_line.startswith('#'):
+ content = s_line.split('#')[0]
+ if content != '':
+ maps.append(content)
+ certmaps = self._parse_maps(maps)
+ return certmaps
+
+ def set(self, certmaps):
+ """
+ Take a dict of certmaps and write them out.
+ """
+ output = ""
+ for name in certmaps:
+ certmap = certmaps[name]
+ output += "certmap %s %s\n" % (name, certmap['issuer'])
+ for v in ['DNComps', 'FilterComps']:
+ if certmap[v] == None:
+ output += "# %s:%s\n" % (name, v)
+ elif certmap[v] == '':
+ output += "%s:%s\n" % (name, v)
+ else:
+ output += "%s:%s %s\n" % (name, v, ',
'.join(certmap[v]))
+ for v in ['VerifyCert', 'CmapLdapAttr', 'Library',
'InitFn']:
+ if certmap[v] == None:
+ output += "# %s:%s\n" % (name, v)
+ else:
+ output += "%s:%s %s\n" % (name, v, certmap[v])
+ # Now write it out
+ certmap = os.path.join(self._instance.get_config_dir(), 'certmap.conf')
+ with open(certmap, 'w') as f:
+ f.write(output)
+
+
+
diff --git a/src/lib389/lib389/configurations/__init__.py
b/src/lib389/lib389/configurations/__init__.py
new file mode 100644
index 0000000..c7b9067
--- /dev/null
+++ b/src/lib389/lib389/configurations/__init__.py
@@ -0,0 +1,25 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389._constants import INSTALL_LATEST_CONFIG
+from .config_001003006 import c001003006, c001003006_sample_entries
+
+def get_config(version):
+ if (version == INSTALL_LATEST_CONFIG):
+ return c001003006
+ if (version == '001003006'):
+ return c001003006
+ raise Exception('version %s no match' % version)
+
+def get_sample_entries(version):
+ if (version == INSTALL_LATEST_CONFIG):
+ return c001003006_sample_entries
+ if (version == '001003006'):
+ return c001003006_sample_entries
+ raise Exception('version %s no match' % version)
+
diff --git a/src/lib389/lib389/configurations/config.py
b/src/lib389/lib389/configurations/config.py
new file mode 100644
index 0000000..ba289bd
--- /dev/null
+++ b/src/lib389/lib389/configurations/config.py
@@ -0,0 +1,44 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# These are the operation runners for configuring a server to look like a certain
+# version.
+
+# Generally these are one way, upgrade only.
+
+class baseconfig(object):
+ def __init__(self, instance):
+ self._instance = instance
+
+ def apply_config(self, install=False, upgrade=False, interactive=False):
+ # Go through the list of operations
+ # Can we assert the types?
+ for op in self._operations:
+ op.apply(install, upgrade, interactive)
+
+# This just serves as a base
+class configoperation(object):
+ def __init__(self, instance):
+ self._instance = instance
+ self.install = True
+ self.upgrade = True
+ self.description = None
+
+ def apply(self, install, upgrade, interactive):
+ # How do we want to handle interactivity?
+ if not ((install and self.install) or (upgrade and self.upgrade)):
+ instance.debug()
+ return False
+ if interactive:
+ raise Exception('Interaction not yet supported')
+ self._apply()
+
+ def _apply(self):
+ # The consumer must over-ride this.
+ raise Exception('Not implemented!')
+
diff --git a/src/lib389/lib389/configurations/config_001003006.py
b/src/lib389/lib389/configurations/config_001003006.py
new file mode 100644
index 0000000..8c2895f
--- /dev/null
+++ b/src/lib389/lib389/configurations/config_001003006.py
@@ -0,0 +1,139 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from ldap import dn
+
+from .config import baseconfig, configoperation
+from .sample import sampleentries
+
+from lib389.idm.domain import Domain
+from lib389.idm.organisationalunit import OrganisationalUnits
+from lib389.idm.group import UniqueGroups, UniqueGroup
+
+from lib389.plugins import WhoamiPlugin
+
+class c001003006_sample_entries(sampleentries):
+ def __init__(self, instance, basedn):
+ super(c001003006_sample_entries, self).__init__(instance, basedn)
+ self.description = """Apply sample entries matching the 1.3.6
sample data and access controls."""
+
+ # All the checks are done, apply them.
+ def _apply(self):
+ # Create the base domain object
+ domain = Domain(self._instance, dn=self._basedn)
+ # Explode the dn to get the first bit.
+ avas = dn.str2dn(self._basedn)
+ dc_ava = avas[0][0][1]
+
+ domain.create(properties={
+ # I think in python 2 this forces unicode return ...
+ 'dc': dc_ava,
+ 'description': self._basedn,
+ 'aci' : '(targetattr ="*")(version 3.0;acl
"Directory Administrators Group";allow (all) (groupdn =
"ldap:///cn=Directory Administrators,
%{BASEDN}");)'.format(BASEDN=self._basedn)
+ })
+ # Create the OUs
+ ous = OrganisationalUnits(self._instance, self._basedn)
+ ous.create(properties = {
+ 'ou': 'Groups',
+ })
+ ous.create(properties = {
+ 'ou': 'People',
+ 'aci' : [
+ '(targetattr ="userpassword || telephonenumber ||
facsimiletelephonenumber")(version 3.0;acl "Allow self entry
modification";allow (write)(userdn = "ldap:///self");)',
+ '(targetattr !="cn || sn || uid")(targetfilter
="(ou=Accounting)")(version 3.0;acl "Accounting Managers Group
Permissions";allow (write)(groupdn = "ldap:///cn=Accounting
Managers,ou=groups,%{BASEDN}");)'.format(BASEDN=self._basedn),
+ '(targetattr !="cn || sn || uid")(targetfilter
="(ou=Human Resources)")(version 3.0;acl "HR Group Permissions";allow
(write)(groupdn = "ldap:///cn=HR
Managers,ou=groups,%{BASEDN}");)'.format(BASEDN=self._basedn),
+ '(targetattr !="cn ||sn || uid")(targetfilter
="(ou=Product Testing)")(version 3.0;acl "QA Group Permissions";allow
(write)(groupdn = "ldap:///cn=QA
Managers,ou=groups,%{BASEDN}");)'.format(BASEDN=self._basedn),
+ '(targetattr !="cn || sn || uid")(targetfilter
="(ou=Product Development)")(version 3.0;acl "Engineering Group
Permissions";allow (write)(groupdn = "ldap:///cn=PD
Managers,ou=groups,%{BASEDN}");)'.format(BASEDN=self._basedn),
+ ]
+ })
+ ous.create(properties = {
+ 'ou': 'Special Users',
+ 'description' : 'Special Administrative Accounts',
+ })
+ # Create the groups.
+ ugs = UniqueGroups(self._instance, self._basedn)
+ ugs.create(properties = {
+ 'cn': 'Accounting Managers',
+ 'description': 'People who can manage accounting entries',
+ 'ou': 'groups',
+ 'uniqueMember' : self._instance.binddn,
+ })
+ ugs.create(properties = {
+ 'cn': 'HR Managers',
+ 'description': 'People who can manage HR entries',
+ 'ou': 'groups',
+ 'uniqueMember' : self._instance.binddn,
+ })
+ ugs.create(properties = {
+ 'cn': 'QA Managers',
+ 'description': 'People who can manage QA entries',
+ 'ou': 'groups',
+ 'uniqueMember' : self._instance.binddn,
+ })
+ ugs.create(properties = {
+ 'cn': 'PD Managers',
+ 'description': 'People who can manage engineer entries',
+ 'ou': 'groups',
+ 'uniqueMember' : self._instance.binddn,
+ })
+ # Create the directory Admin group.
+ # We can't use the group factory here, as we need a custom DN override.
+ da_ug = UniqueGroup(self._instance)
+ da_ug._dn = 'cn=Directory Administrators,%s' % self._basedn
+ da_ug.create(properties={
+ 'cn': 'Directory Administrators',
+ 'uniqueMember' : self._instance.binddn,
+ })
+ # DONE!
+
+### Operations to be filled in soon!
+
+class c001003006_whoami_plugin(configoperation):
+ def __init__(self, instance):
+ super(c001003006_whoami_plugin, self).__init__(instance)
+ self.install = True
+ self.upgrade = True
+ self.description = "Enable the Whoami Plugin"
+
+ def _apply(self):
+ # This needs to change from create to "ensure"?
+ whoami_plugin = WhoamiPlugin(self._instance)
+ whoami_plugin.create()
+
+class c001003006(baseconfig):
+ def __init__(self, instance):
+ super(c001003006, self).__init__(instance)
+ self._operations = [
+ # Create plugin configs first
+ # c001003006_whoami_plugin(self._instance)
+ # Create our sample entries.
+ # op001003006_sample_entries(self._instance),
+ ]
+
+
+ # We need the following to be created and ENABLED
+ # SyntaxValidationPlugin
+ # SchemaReloadPlugin
+ # StateChangePlugin
+ # RolesPlugin
+ # ACLPlugin
+ # ACLPreoperationPlugin
+ # ClassOfServicePlugin
+ # ViewsPlugin
+ # SevenBitCheckPlugin
+ # AccountUsabilityPlugin
+ # AutoMembershipPlugin
+ # DereferencePlugin
+ # HTTPClientPlugin
+ # LinkedAttributesPlugin
+ # ManagedEntriesPlugin
+ # WhoamiPlugin
+ # LDBMBackendPlugin
+ # ChainingBackendPlugin
+
+
diff --git a/src/lib389/lib389/configurations/sample.py
b/src/lib389/lib389/configurations/sample.py
new file mode 100644
index 0000000..a2292f5
--- /dev/null
+++ b/src/lib389/lib389/configurations/sample.py
@@ -0,0 +1,21 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389.utils import ensure_str
+
+class sampleentries(object):
+ def __init__(self, instance, basedn):
+ self._instance = instance
+ self._basedn = ensure_str(basedn)
+ self.description = None
+
+ def apply(self):
+ self._apply()
+
+ def _apply(self):
+ raise Exception('Not implemented')
diff --git a/src/lib389/lib389/dbgen.py b/src/lib389/lib389/dbgen.py
new file mode 100644
index 0000000..a0cda94
--- /dev/null
+++ b/src/lib389/lib389/dbgen.py
@@ -0,0 +1,191 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# Replacement of the dbgen.pl utility
+
+import random
+import os
+import pwd
+import grp
+
+DBGEN_POSITIONS = [
+"Accountant",
+"Admin",
+"Architect",
+"Assistant",
+"Artist",
+"Consultant",
+"Czar",
+"Dictator",
+"Director",
+"Diva",
+"Dreamer",
+"Evangelist",
+"Engineer",
+"Figurehead",
+"Fellow",
+"Grunt",
+"Guru",
+"Janitor",
+"Madonna",
+"Manager",
+"Pinhead",
+"President",
+"Punk",
+"Sales Rep",
+"Stooge",
+"Visionary",
+"Vice President",
+"Writer",
+"Warrior",
+"Yahoo"
+]
+
+DBGEN_TITLE_LEVELS = [
+"Senior",
+"Master",
+"Associate",
+"Junior",
+"Chief",
+"Supreme",
+"Elite"
+]
+
+DBGEN_LOCATIONS = [
+"Mountain View", "Redmond", "Redwood Shores",
"Armonk",
+"Cambridge", "Santa Clara", "Sunnyvale",
"Alameda",
+"Cupertino", "Menlo Park", "Palo Alto", "Orem",
+"San Jose", "San Francisco", "Milpitas",
"Hartford", "Windsor",
+"Boston", "New York", "Detroit", "Dallas",
"Denver", "Brisbane",
+]
+
+DBGEN_OUS = [
+"Accounting",
+"Product Development",
+"Product Testing",
+"Human Resources",
+"Payroll",
+]
+
+DBGEN_TEMPLATE = """dn: {DN}
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+cn: {FIRST} {LAST}
+sn: {LAST}
+uid: {UID}
+givenName: {FIRST}
+description: 2;7613;CN=Red Hat CS 71GA Demo,O=Red Hat CS 71GA Demo,C=US;CN=RHCS Agent -
admin01,UID=admin01,O=redhat,C=US [1] This is {FIRST} {LAST}'s description.
+userPassword: {UID}
+departmentNumber: 1230
+employeeType: Manager
+homePhone: +1 303 937-6482
+initials: {INITIALS}
+telephoneNumber: +1 303 573-9570
+facsimileTelephoneNumber: +1 415 408-8176
+mobile: +1 818 618-1671
+pager: +1 804 339-6298
+roomNumber: 5164
+carLicense: 21SJJAG
+l: {LOCATION}
+ou: {OU}
+mail: {FIRST}.{LAST}(a)example.com
+postalAddress: 518, Dept #851, Room#{OU}
+title: {TITLE}
+usercertificate;binary:: MIIBvjCCASegAwIBAgIBAjANBgkqhkiG9w0BAQQFADAnMQ8wDQYD
+ VQQDEwZjb25maWcxFDASBgNVBAMTC01NUiBDQSBDZXJ0MB4XDTAxMDQwNTE1NTEwNloXDTExMDcw
+ NTE1NTEwNlowIzELMAkGA1UEChMCZnIxFDASBgNVBAMTC01NUiBTMSBDZXJ0MIGfMA0GCSqGSIb3
+ DQEBAQUAA4GNADCBiQKBgQDNlmsKEaPD+o3mAUwmW4E40MPs7aiui1YhorST3KzVngMqe5PbObUH
+ MeJN7CLbq9SjXvdB3y2AoVl/s5UkgGz8krmJ8ELfUCU95AQls321RwBdLRjioiQ3MGJiFjxwYRIV
+ j1CUTuX1y8dC7BWvZ1/EB0yv0QDtp2oVMUeoK9/9sQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBADev
+ hxY6QyDMK3Mnr7vLGe/HWEZCObF+qEo2zWScGH0Q+dAmhkCCkNeHJoqGN4NWjTdnBcGaAr5Y85k1
+ o/vOAMBsZePbYx4SrywL0b/OkOmQX+mQwieC2IQzvaBRyaNMh309vrF4w5kExReKfjR/gXpHiWQz
+ GSxC5LeQG4k3IP34
+
+"""
+
+DBGEN_HEADER = """dn: {SUFFIX}
+objectClass: top
+objectClass: domain
+dc: example
+aci: (target=ldap:///{SUFFIX})(targetattr=*)(version 3.0; acl "acl1";
allow(write) userdn = "ldap:///self";)
+aci: (target=ldap:///{SUFFIX})(targetattr=*)(version 3.0; acl "acl2";
allow(write) groupdn = "ldap:///cn=Directory Administrators, {SUFFIX}";)
+aci: (target=ldap:///{SUFFIX})(targetattr=*)(version 3.0; acl "acl3";
allow(read, search, compare) userdn = "ldap:///anyone";)
+
+dn: ou=Accounting,{SUFFIX}
+objectClass: top
+objectClass: organizationalUnit
+ou: Accounting
+
+dn: ou=Product Development,{SUFFIX}
+objectClass: top
+objectClass: organizationalUnit
+ou: Product Development
+
+dn: ou=Product Testing,{SUFFIX}
+objectClass: top
+objectClass: organizationalUnit
+ou: Product Testing
+
+dn: ou=Human Resources,{SUFFIX}
+objectClass: top
+objectClass: organizationalUnit
+ou: Human Resources
+
+dn: ou=Payroll,{SUFFIX}
+objectClass: top
+objectClass: organizationalUnit
+ou: Payroll
+
+"""
+
+def dbgen(instance, number, ldif_file, suffix):
+ familyname_file = os.path.join(instance.ds_paths.data_dir,
'dirsrv/data/dbgen-FamilyNames')
+ givename_file = os.path.join(instance.ds_paths.data_dir,
'dirsrv/data/dbgen-GivenNames')
+ familynames = []
+ givennames = []
+ with open(familyname_file, 'r') as f:
+ familynames = [n.strip() for n in f]
+ with open(givename_file, 'r') as f:
+ givennames = [n.strip() for n in f]
+
+ with open(ldif_file, 'w') as output:
+ output.write(DBGEN_HEADER.format(SUFFIX=suffix))
+ for i in range(0, number):
+ # Pick a random ou
+ ou = random.choice(DBGEN_OUS)
+ first = random.choice(givennames)
+ last = random.choice(familynames)
+ # How do we subscript from a generator?
+ initials = "%s. %s" % (first[0], last[0])
+ uid = "%s%s%s" % (first[0], last, i)
+ dn = "uid=%s,ou=%s,%s" % (uid, ou, suffix)
+ l = random.choice(DBGEN_LOCATIONS)
+ title = "%s %s" % (random.choice(DBGEN_TITLE_LEVELS),
random.choice(DBGEN_POSITIONS))
+ output.write(DBGEN_TEMPLATE.format(
+ DN=dn,
+ UID=uid,
+ FIRST=first,
+ LAST=last,
+ INITIALS=initials,
+ OU=ou,
+ LOCATION=l,
+ TITLE=title,
+ SUFFIX=suffix
+ ))
+
+ # Make the file owned by dirsrv
+ os.chmod(ldif_file, 0o644)
+ if os.getuid() == 0:
+ # root user - chown the ldif to the server user
+ uid = pwd.getpwnam(instance.userid).pw_uid
+ gid = grp.getgrnam(instance.userid).gr_gid
+ os.chown(ldif_file, uid, gid)
+
+
diff --git a/src/lib389/lib389/dirsrv_log.py b/src/lib389/lib389/dirsrv_log.py
new file mode 100644
index 0000000..6d1e070
--- /dev/null
+++ b/src/lib389/lib389/dirsrv_log.py
@@ -0,0 +1,271 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""Helpers for managing the directory server internal logs.
+"""
+
+import re
+import gzip
+from datetime import datetime
+from dateutil.parser import parse as dt_parse
+from glob import glob
+from lib389._constants import DN_CONFIG
+from lib389.utils import ensure_bytes, ensure_str
+
+
+# Because many of these settings can change live, we need to check for certain
+# attributes all the time.
+
+MONTH_LOOKUP = {
+ 'Jan': 1,
+ 'Feb': 2,
+ 'Mar': 3,
+ 'Apr': 4,
+ 'May': 5,
+ 'Jun': 6,
+ 'Jul': 7,
+ 'Aug': 8,
+ 'Oct': 9,
+ 'Sep': 10,
+ 'Nov': 11,
+ 'Dec': 12,
+}
+
+
+class DirsrvLog(object):
+ """Class of functions to working with the various DIrectory Server
logs
+ """
+ def __init__(self, dirsrv):
+ """Initial class
+ @param dirsrv - DirSrv object
+ """
+ self.dirsrv = dirsrv
+ self.log = self.dirsrv.log
+ self.prog_timestamp =
re.compile('\[(?P<day>\d*)\/(?P<month>\w*)\/(?P<year>\d*):(?P<hour>\d*):(?P<minute>\d*):(?P<second>\d*)(.(?P<nanosecond>\d*))+\s(?P<tz>[\+\-]\d*)')
# noqa
+ self.prog_datetime = re.compile('^(?P<timestamp>\[.*\])')
+
+ def _get_log_path(self):
+ """Return the current log file location"""
+ raise Exception("Log type not defined.")
+
+ def _get_all_log_paths(self):
+ """Return all the log paths"""
+ return glob("%s.*-*" % self._get_log_path()) + [self._get_log_path()]
+
+ def readlines_archive(self):
+ """
+ Returns an array of all the lines in all logs, included rotated logs
+ and compressed logs. (gzip)
+ Will likely be very slow. Try using match instead.
+
+ @return - an array of all the lines in all logs
+ """
+ lines = []
+ for log in self._get_all_log_paths():
+ # Open the log
+ if log.endswith(ensure_bytes('.gz')):
+ with gzip.open(log, 'r') as lf:
+ lines += lf.readlines()
+ else:
+ with open(log, 'r') as lf:
+ lines += lf.readlines()
+ return lines
+
+ def readlines(self):
+ """Returns an array of all the lines in the log.
+ Will likely be very slow. Try using match instead.
+
+ @return - an array of all the lines in the log.
+ """
+ lines = []
+ self.lpath = self._get_log_path()
+ if self.lpath is not None:
+ # Open the log
+ with open(self.lpath, 'r') as lf:
+ lines = lf.readlines()
+ return lines
+
+ def match_archive(self, pattern):
+ """Search all the log files, including "zipped" logs
+ @param pattern - a regex pattern
+ @return - results of the pattern matching
+ """
+ results = []
+ prog = re.compile(pattern)
+ for log in self._get_all_log_paths():
+ if log.endswith(ensure_bytes('.gz')):
+ with gzip.open(log, 'r') as lf:
+ for line in lf:
+ mres = prog.match(line)
+ if mres:
+ results.append(line)
+ else:
+ with open(log, 'r') as lf:
+ for line in lf:
+ mres = prog.match(line)
+ if mres:
+ results.append(line)
+ return results
+
+ def match(self, pattern):
+ """Search the current log file for the pattern
+ @param pattern - a regex pattern
+ @return - results of the pattern matching
+ """
+ results = []
+ prog = re.compile(pattern)
+ self.lpath = self._get_log_path()
+ if self.lpath is not None:
+ with open(self.lpath, 'r') as lf:
+ for line in lf:
+ mres = prog.match(line)
+ if mres:
+ results.append(line)
+ return results
+
+ def parse_timestamp(self, ts):
+ """Parse a logs timestamps and break it down into its individual
parts
+ @param ts - The timestamp string from a log
+ @return - a "datetime" object
+ """
+ timedata = self.prog_timestamp.match(ts).groupdict()
+ # Now, have to convert month to an int.
+ dt_str = '{YEAR}-{MONTH}-{DAY} {HOUR}-{MINUTE}-{SECOND} {TZ}'.format(
+ YEAR=timedata['year'],
+ MONTH=MONTH_LOOKUP[timedata['month']],
+ DAY=timedata['day'],
+ HOUR=timedata['hour'],
+ MINUTE=timedata['minute'],
+ SECOND=timedata['second'],
+ TZ=timedata['tz'],
+ )
+ dt = dt_parse(dt_str)
+ if timedata['nanosecond'] is not '':
+ dt = dt.replace(microsecond=int(int(timedata['nanosecond']) / 1000))
+ return dt
+
+ def get_time_in_secs(self, log_line):
+ """Take the timestamp (not the date) from a DS log and convert it
+ to seconds:
+
+ [25/May/2016:15:24:27.289341875 -0400]...
+
+ @param log_line - A line of txt from a DS error/access log
+ @return - time in seconds
+ """
+
+ total = 0
+ index = log_line.index(':') + 1
+ hms = log_line[index: index + 8]
+ parts = hms.split(':')
+ if int(parts[0]):
+ total += int(parts[0]) * 3600
+ if int(parts[1]):
+ total += int(parts[1]) * 60
+ total += int(parts[2])
+
+ return total
+
+
+class DirsrvAccessLog(DirsrvLog):
+ """Class for process access logs"""
+ def __init__(self, dirsrv):
+ """Init the class
+ @param dirsrv - A DirSrv object
+ """
+ super(DirsrvAccessLog, self).__init__(dirsrv)
+ ## We precompile our regex for parse_line to make it faster.
+ self.prog_m1 =
re.compile('^(?P<timestamp>\[.*\])\sconn=(?P<conn>\d*)\sop=(?P<op>\d*)\s(?P<action>\w*)\s(?P<rem>.*)')
+ self.prog_con =
re.compile('^(?P<timestamp>\[.*\])\sconn=(?P<conn>\d*)\sfd=(?P<fd>\d*)\sslot=(?P<slot>\d*)\sconnection\sfrom\s(?P<remote>[^\s]*)\sto\s(?P<local>[^\s]*)')
+ self.prog_discon =
re.compile('^(?P<timestamp>\[.*\])\sconn=(?P<conn>\d*)\sop=(?P<op>\d*)\sfd=(?P<fd>\d*)\s(?P<action>closed)\s-\s(?P<status>\w*)')
+ # RESULT regex's (based off action.rem)
+ self.prog_notes =
re.compile('err=(?P<err>\d*)\stag=(?P<tag>\d*)\snentries=(?P<nentries>\d*)\setime=(?P<etime>[0-9.]*)\snotes=(?P<notes>\w*)')
+ self.prog_repl =
re.compile('err=(?P<err>\d*)\stag=(?P<tag>\d*)\snentries=(?P<nentries>\d*)\setime=(?P<etime>[0-9.]*)\scsn=(?P<csn>\w*)')
+ self.prog_result =
re.compile('err=(?P<err>\d*)\stag=(?P<tag>\d*)\snentries=(?P<nentries>\d*)\setime=(?P<etime>[0-9.]*)\s(?P<rem>.*)')
+ # Lists for each regex type
+ self.full_regexs = [self.prog_m1, self.prog_con, self.prog_discon]
+ self.result_regexs = [self.prog_notes, self.prog_repl,
+ self.prog_result]
+
+ def _get_log_path(self):
+ """Return the current log file location"""
+ return self.dirsrv.ds_paths.access_log
+
+ def parse_line(self, line):
+ """
+ This knows how to break up an access log line into the specific fields.
+ @param line - A text line from an access log
+ @return - A dictionary of the log parts
+ """
+ line = line.strip()
+ action = {
+ 'action': 'CONNECT'
+ }
+ # First, pull some well known info out.
+ if self.dirsrv.verbose:
+ self.log.info("--> %s " % line)
+
+ for regex in self.full_regexs:
+ result = regex.match(line)
+ if result:
+ action.update(result.groupdict())
+ if regex == self.prog_discon:
+ action['action'] = 'DISCONNECT'
+ break
+
+ if action['action'] == 'RESULT':
+ for regex in self.result_regexs:
+ result = regex.match(action['rem'])
+ if result:
+ action.update(result.groupdict())
+ break
+
+ action['datetime'] = self.parse_timestamp(action['timestamp'])
+
+ if self.dirsrv.verbose:
+ self.log.info(action)
+ return action
+
+ def parse_lines(self, lines):
+ """Parse multiple log lines
+ @param lines - a list of log lines
+ @return - A dictionary of the log parts for each line
+ """
+ return map(self.parse_line, lines)
+
+
+class DirsrvErrorLog(DirsrvLog):
+ """Directory Server Error log class"""
+ def __init__(self, dirsrv):
+ """Init the Error log class
+ @param diursrv - A DirSrv object
+ """
+ super(DirsrvErrorLog, self).__init__(dirsrv)
+ self.prog_m1 =
re.compile('^(?P<timestamp>\[.*\])\s(?P<message>.*)')
+
+ def _get_log_path(self):
+ """Return the current log file location"""
+ return self.dirsrv.ds_paths.error_log
+
+ def parse_line(self, line):
+ """Parse an errors log line
+ @line - a text string from an errors log
+ @return - A dictionary of the log parts
+ """
+ line = line.strip()
+ action = self.prog_m1.match(line).groupdict()
+
+ action['datetime'] = self.parse_timestamp(action['timestamp'])
+ return action
+
+ def parse_lines(self, lines):
+ """Parse multiple lines from an errors log
+ @param lines - a lits of strings/lines from an errors log
+ @return - A dictionary of the log parts for each line
+ """
+ return map(self.parse_line, lines)
diff --git a/src/lib389/lib389/ds_instance.py b/src/lib389/lib389/ds_instance.py
new file mode 100644
index 0000000..d4601da
--- /dev/null
+++ b/src/lib389/lib389/ds_instance.py
@@ -0,0 +1,97 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import os
+import re
+import sys
+import collections
+
+
+class DSDecorator(object):
+ """ Implements decorator that can be instantiated during runtime.
+
+ Accepts function and instance of DSInstance. When called,
+ it will call passed function with instance of DSInstance
+ passed as the first argument.
+ """
+ def __init__(self, fun, ds_instance):
+ self.fun = fun
+ self.ds_instance = ds_instance
+
+ def __call__(self, *args, **kwargs):
+ if args is None and kwargs is None:
+ return self.fun(self.ds_instance)
+ elif args is not None and kwargs is None:
+ return self.fun(self.ds_instance, *args)
+ elif args is None and kwargs is not None:
+ return self.fun(self.ds_instance, **kwargs)
+ else:
+ return self.fun(self.ds_instance, *args, **kwargs)
+
+
+class DSModuleProxy(object):
+ """ Proxy with DS-decorated functions from modules in directory.
+
+ DSModuleProxy object acts as a proxy to functions defined in modules
+ stored in directory. Proxy itself can have other proxies as
+ attributes, so returned object follows the structure of passed
+ directory. All funcions from all modules are decorated with
+ DSDecorator - DSInstance object will be passed to them as first
+ argument.
+
+ Kudos to Milan Falesnik <mfalesni(a)redhat.com>
+ """
+
+ def __init__(self, module_name):
+ self.name = module_name
+
+ @classmethod
+ def populate_with_proxies(cls, ds, obj, directory):
+ """
+ Returns a proxy with decorated functions from modules in directory.
+ """
+ if not os.path.isdir(directory):
+ raise RuntimeError("Last argument %s was not directory" %
+ directory)
+ for item in os.listdir(directory):
+
+ # case when item is directory
+ if os.path.isdir(directory + "/" + item):
+ # Recursively call self on all subdirectories
+ proxy = cls(item)
+ cls.populate_with_proxies(ds, proxy, directory + "/" + item)
+ setattr(obj, item, proxy)
+
+ # case when item is python module
+ elif (os.path.isfile(directory + "/" + item) and
+ re.match(r'(.*\/)?[^_/].*[^_/]\.py$', item)):
+
+ # get the module object corresponding to processed file
+ item = item.replace('.py', '')
+ to_import = (directory + "/" + item).replace('/',
'.')
+ __import__(to_import)
+ module = sys.modules[to_import]
+ proxy = cls(item)
+
+ # for each function from module create decorated one
+ for attr in dir(module):
+ fun = getattr(module, attr)
+ if isinstance(fun, collections.Callable):
+ decorated = DSDecorator(fun, ds)
+ setattr(proxy, attr, decorated)
+ setattr(obj, item, proxy)
+
+ else:
+ # skip anything that is not directory or python module
+ pass
+
+
+class DSInstance(object):
+
+ def __init__(self):
+ DSModuleProxy.populate_with_proxies(self, self, 'dsmodules')
diff --git a/src/lib389/lib389/dseldif.py b/src/lib389/lib389/dseldif.py
new file mode 100644
index 0000000..7e5f6ab
--- /dev/null
+++ b/src/lib389/lib389/dseldif.py
@@ -0,0 +1,101 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import os
+from lib389.paths import Paths
+
+
+class DSEldif(object):
+ """A class for working with dse.ldif file"""
+
+ def __init__(self, instance):
+ self._instance = instance
+
+ ds_paths = Paths(self._instance.serverid, self._instance)
+ self.path = os.path.join(ds_paths.config_dir, 'dse.ldif')
+
+ with open(self.path, 'r') as file_dse:
+ self._contents = file_dse.readlines()
+
+ def _update(self):
+ """Update the dse.ldif with a new contents"""
+
+ with open(self.path, "w") as file_dse:
+ file_dse.write("".join(self._contents))
+
+ def _find_attr(self, entry_dn, attr):
+ """Find all attribute values and indexes under a given entry
+
+ Returns entry dn index and attribute data dict:
+ relative attribute indexes and the attribute value
+ """
+
+ entry_dn_i = self._contents.index("dn: {}\n".format(entry_dn))
+ attr_data = {}
+
+ # Find where the entry ends
+ try:
+ dn_end_i = self._contents[entry_dn_i:].index("\n")
+ except ValueError:
+ # We are in the end of the list
+ dn_end_i = len(self._contents)
+
+ entry_slice = self._contents[entry_dn_i:entry_dn_i + dn_end_i]
+
+ # Find the attribute
+ for line in entry_slice:
+ if line.startswith("{}:".format(attr)):
+ attr_value = line.split(" ", 1)[1][:-1]
+ attr_data.update({entry_slice.index(line): attr_value})
+
+ if not attr_data:
+ raise ValueError("Attribute {} wasn't found under dn:
{}".format(attr, entry_dn))
+
+ return entry_dn_i, attr_data
+
+ def get(self, entry_dn, attr):
+ """Return attribute values under a given entry"""
+
+ try:
+ _, attr_data = self._find_attr(entry_dn, attr)
+ except ValueError:
+ return None
+
+ return attr_data.values()
+
+ def add(self, entry_dn, attr, value):
+ """Add an attribute under a given entry"""
+
+ entry_dn_i = self._contents.index("dn: {}\n".format(entry_dn))
+ self._contents.insert(entry_dn_i+1, "{}: {}\n".format(attr, value))
+ self._update()
+
+ def delete(self, entry_dn, attr, value=None):
+ """Delete attributes under a given entry"""
+
+ entry_dn_i, attr_data = self._find_attr(entry_dn, attr)
+
+ if value is not None:
+ for attr_i, attr_value in attr_data.items():
+ if attr_value == value:
+ del self._contents[entry_dn_i + attr_i]
+ else:
+ for attr_i in sorted(attr_data.keys(), reverse=True):
+ del self._contents[entry_dn_i + attr_i]
+ self._update()
+
+ def replace(self, entry_dn, attr, value):
+ """Replace attribute values with a new one under a given
entry"""
+
+ try:
+ self.delete(entry_dn, attr)
+ except ValueError as e:
+ self._instance.log.debug("During replace operation: {}".format(e))
+ self.add(entry_dn, attr, value)
+ self._update()
+
diff --git a/src/lib389/lib389/exceptions.py b/src/lib389/lib389/exceptions.py
new file mode 100644
index 0000000..e055193
--- /dev/null
+++ b/src/lib389/lib389/exceptions.py
@@ -0,0 +1,43 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+
+
+class Error(Exception):
+ pass
+
+
+class InvalidArgumentError(Error):
+ pass
+
+
+class AlreadyExists(ldap.ALREADY_EXISTS):
+ pass
+
+
+class NoSuchEntryError(ldap.NO_SUCH_OBJECT):
+ pass
+
+
+class MissingEntryError(NoSuchEntryError):
+ """When just added entries are missing."""
+ pass
+
+
+class UnwillingToPerformError(Error):
+ pass
+
+
+class NotImplementedError(Error):
+ pass
+
+
+class DsError(Error):
+ """Generic DS Error."""
+ pass
diff --git a/src/lib389/lib389/extended_operations.py
b/src/lib389/lib389/extended_operations.py
new file mode 100644
index 0000000..31e1cb3
--- /dev/null
+++ b/src/lib389/lib389/extended_operations.py
@@ -0,0 +1,67 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""
+Lib389 python ldap extended operation controls
+
+These should be upstreamed if possible.
+"""
+
+from ldap.extop import ExtendedRequest, ExtendedResponse
+from pyasn1.type import namedtype, univ, tag
+from pyasn1.codec.ber import encoder, decoder
+from pyasn1_modules.rfc2251 import AttributeDescription, LDAPDN, AttributeValue
+from lib389._constants import *
+
+# Tag id's should match
https://www.obj-sys.com/asn1tutorial/node124.html
+
+class LdapSSOTokenRequestValue(univ.Sequence):
+ componentType = namedtype.NamedTypes(
+ namedtype.NamedType('ValidLifeTime', univ.Integer().subtype(
+ implicitTag=tag.Tag(tag.tagClassUniversal,tag.tagFormatSimple,0)
+ )
+ ),
+ )
+
+class LdapSSOTokenResponseValue(univ.Sequence):
+ componentType = namedtype.NamedTypes(
+ namedtype.NamedType('ValidLifeTime', univ.Integer().subtype(
+ implicitTag=tag.Tag(tag.tagClassUniversal,tag.tagFormatSimple,2)
+ )
+ ),
+ namedtype.NamedType('EncryptedToken', univ.OctetString().subtype(
+ implicitTag=tag.Tag(tag.tagClassUniversal,tag.tagFormatSimple,4)
+ )
+ ),
+ )
+
+class LdapSSOTokenRequest(ExtendedRequest):
+ requestName = '2.16.840.1.113730.3.5.14'
+ def __init__(self, requestValidLifeTime=0):
+ self.requestValidLifeTime = requestValidLifeTime
+
+ def encodedRequestValue(self):
+ v = LdapSSOTokenRequestValue()
+ v.setComponentByName('ValidLifeTime',
univ.Integer(self.requestValidLifeTime).subtype(
+ implicitTag=tag.Tag(tag.tagClassUniversal,tag.tagFormatSimple,0)
+ )
+ )
+ return encoder.encode(v)
+
+class LdapSSOTokenResponse(ExtendedResponse):
+ responseName = '2.16.840.1.113730.3.5.15'
+
+ def decodeResponseValue(self, value):
+ response_value, _ = decoder.decode(value,asn1Spec=LdapSSOTokenResponseValue())
+ self.validLifeTime =
int(response_value.getComponentByName('ValidLifeTime'))
+ self.token = response_value.getComponentByName('EncryptedToken')
+ return (self.validLifeTime, self.token)
+
+class LdapSSOTokenRevokeRequest(ExtendedRequest):
+ requestName = '2.16.840.1.113730.3.5.16'
+
diff --git a/src/lib389/lib389/idm/__init__.py b/src/lib389/lib389/idm/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib389/lib389/idm/account.py b/src/lib389/lib389/idm/account.py
new file mode 100644
index 0000000..02e1874
--- /dev/null
+++ b/src/lib389/lib389/idm/account.py
@@ -0,0 +1,50 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389._mapped_object import DSLdapObject, DSLdapObjects, _gen_or, _gen_filter,
_term_gen
+
+
+class Account(DSLdapObject):
+ def is_locked(self):
+ # Check if nsAccountLock is set.
+ return self.present('nsAccountLock')
+
+ def lock(self):
+ self.replace('nsAccountLock', 'true')
+
+ def unlock(self):
+ self.remove('nsAccountLock', None)
+
+class Accounts(DSLdapObjects):
+ def __init__(self, instance, basedn, batch=False):
+ super(Accounts, self).__init__(instance, batch)
+ # These are all the objects capable of holding a password.
+ self._objectclasses = [
+ 'simpleSecurityObject',
+ 'organization',
+ 'personperson',
+ 'organizationalUnit',
+ 'netscapeServer',
+ 'domain',
+ 'posixAccount',
+ 'shadowAccount',
+ 'posixGroup',
+ 'mailRecipient',
+ ]
+ # MUST BE NONE.
+ self._filterattrs = None
+ self._childobject = Account
+ self._basedn = basedn
+
+ #### This is copied from DSLdapObjects, but change _gen_and to _gen_or!!!
+
+ def _get_objectclass_filter(self):
+ return _gen_or(
+ _gen_filter(_term_gen('objectclass'), self._objectclasses)
+ )
+
diff --git a/src/lib389/lib389/idm/domain.py b/src/lib389/lib389/idm/domain.py
new file mode 100644
index 0000000..480c1c8
--- /dev/null
+++ b/src/lib389/lib389/idm/domain.py
@@ -0,0 +1,21 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389._mapped_object import DSLdapObject, DSLdapObjects
+
+class Domain(DSLdapObject):
+ def __init__(self, instance, dn=None, batch=False):
+ super(Domain, self).__init__(instance, dn, batch)
+ self._rdn_attribute = 'dc'
+ self._must_attributes = ['dc']
+ self._create_objectclasses = [
+ 'top',
+ 'domain',
+ ]
+ self._protected = True
+
diff --git a/src/lib389/lib389/idm/group.py b/src/lib389/lib389/idm/group.py
new file mode 100644
index 0000000..5de1294
--- /dev/null
+++ b/src/lib389/lib389/idm/group.py
@@ -0,0 +1,89 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389._mapped_object import DSLdapObject, DSLdapObjects
+from lib389.utils import ds_is_older, ensure_str
+
+MUST_ATTRIBUTES = [
+ 'cn',
+]
+RDN = 'cn'
+
+class Group(DSLdapObject):
+ def __init__(self, instance, dn=None, batch=False):
+ super(Group, self).__init__(instance, dn, batch)
+ self._rdn_attribute = RDN
+ # Can I generate these from schema?
+ self._must_attributes = MUST_ATTRIBUTES
+ self._create_objectclasses = [
+ 'top',
+ 'groupOfNames',
+ ]
+ if not ds_is_older('1.3.7'):
+ self._create_objectclasses.append('nsMemberOf')
+ self._protected = False
+
+ def is_member(self, dn):
+ # Check if dn is a member
+ return self.present('member', dn)
+
+ def add_member(self, dn):
+ self.add('member', dn)
+
+ def remove_member(self, dn):
+ self.remove('member', dn)
+
+class Groups(DSLdapObjects):
+ def __init__(self, instance, basedn, batch=False, rdn='ou=Groups'):
+ super(Groups, self).__init__(instance, batch)
+ self._objectclasses = [
+ 'groupOfNames',
+ ]
+ self._filterattrs = [RDN]
+ self._childobject = Group
+ self._basedn = '{},{}'.format(ensure_str(rdn), ensure_str(basedn))
+
+class UniqueGroup(DSLdapObject):
+ # WARNING!!!
+ # Use group, not unique group!!!
+ def __init__(self, instance, dn=None, batch=False):
+ super(UniqueGroup, self).__init__(instance, dn, batch)
+ self._rdn_attribute = RDN
+ self._must_attributes = MUST_ATTRIBUTES
+ self._create_objectclasses = [
+ 'top',
+ 'groupOfUniqueNames',
+ ]
+ if not ds_is_older('1.3.7'):
+ self._create_objectclasses.append('nsMemberOf')
+ self._protected = False
+
+ def is_member(self, dn):
+ # Check if dn is a member
+ return self.present('uniquemember', dn)
+
+ def add_member(self, dn):
+ self.add('uniquemember', dn)
+
+ def remove_member(self, dn):
+ self.remove('uniquemember', dn)
+
+class UniqueGroups(DSLdapObjects):
+ # WARNING!!!
+ # Use group, not unique group!!!
+ def __init__(self, instance, basedn, batch=False, rdn='ou=Groups'):
+ super(UniqueGroups, self).__init__(instance, batch)
+ self._objectclasses = [
+ 'groupOfUniqueNames',
+ ]
+ self._filterattrs = [RDN]
+ self._childobject = UniqueGroup
+ self._basedn = '{},{}'.format(rdn, basedn)
+
+
+
diff --git a/src/lib389/lib389/idm/organisationalunit.py
b/src/lib389/lib389/idm/organisationalunit.py
new file mode 100644
index 0000000..a3854c9
--- /dev/null
+++ b/src/lib389/lib389/idm/organisationalunit.py
@@ -0,0 +1,38 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389._mapped_object import DSLdapObject, DSLdapObjects
+
+MUST_ATTRIBUTES = [
+ 'ou',
+]
+RDN = 'ou'
+
+class OrganisationalUnit(DSLdapObject):
+ def __init__(self, instance, dn=None, batch=False):
+ super(OrganisationalUnit, self).__init__(instance, dn, batch)
+ self._rdn_attribute = RDN
+ # Can I generate these from schema?
+ self._must_attributes = MUST_ATTRIBUTES
+ self._create_objectclasses = [
+ 'top',
+ 'organizationalunit',
+ ]
+ self._protected = False
+
+class OrganisationalUnits(DSLdapObjects):
+ def __init__(self, instance, basedn, batch=False):
+ super(OrganisationalUnits, self).__init__(instance, batch)
+ self._objectclasses = [
+ 'organizationalunit',
+ ]
+ self._filterattrs = [RDN]
+ self._childobject = OrganisationalUnit
+ self._basedn = basedn
+
+
diff --git a/src/lib389/lib389/idm/posixgroup.py b/src/lib389/lib389/idm/posixgroup.py
new file mode 100644
index 0000000..8605f63
--- /dev/null
+++ b/src/lib389/lib389/idm/posixgroup.py
@@ -0,0 +1,52 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389._mapped_object import DSLdapObject, DSLdapObjects
+from lib389.utils import ds_is_older
+
+MUST_ATTRIBUTES = [
+ 'cn',
+ 'gidNumber',
+]
+RDN = 'cn'
+
+class PosixGroup(DSLdapObject):
+ def __init__(self, instance, dn=None, batch=False):
+ super(PosixGroup, self).__init__(instance, dn, batch)
+ self._rdn_attribute = RDN
+ # Can I generate these from schema?
+ self._must_attributes = MUST_ATTRIBUTES
+ self._create_objectclasses = [
+ 'top',
+ 'groupOfNames',
+ 'posixGroup',
+ ]
+ if not ds_is_older('1.3.7'):
+ self._create_objectclasses.append('nsMemberOf')
+ self._protected = False
+
+ def check_member(self, dn):
+ return dn in self.get_attr_vals('member')
+
+ def add_member(self, dn):
+ # Assert the DN exists?
+ self.add('member', dn)
+
+
+class PosixGroups(DSLdapObjects):
+ def __init__(self, instance, basedn, batch=False, rdn='ou=Groups'):
+ super(PosixGroups, self).__init__(instance, batch)
+ self._objectclasses = [
+ 'groupOfNames',
+ 'posixGroup',
+ ]
+ self._filterattrs = [RDN]
+ self._childobject = PosixGroup
+ self._basedn = '{},{}'.format(rdn, basedn)
+
+
diff --git a/src/lib389/lib389/idm/services.py b/src/lib389/lib389/idm/services.py
new file mode 100644
index 0000000..129dad2
--- /dev/null
+++ b/src/lib389/lib389/idm/services.py
@@ -0,0 +1,36 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389._mapped_object import DSLdapObject, DSLdapObjects
+
+RDN = 'cn'
+MUST_ATTRIBUTES = [
+ 'cn',
+]
+
+class ServiceAccount(DSLdapObject):
+ def __init__(self, instance, dn=None, batch=False):
+ super(ServiceAccount, self).__init__(instance, dn, batch)
+ self._rdn_attribute = RDN
+ self._must_attributes = MUST_ATTRIBUTES
+ self._create_objectclasses = [
+ 'top',
+ 'netscapeServer',
+ ]
+ self._protected = False
+
+class ServiceAccounts(DSLdapObjects):
+ def __init__(self, instance, basedn, batch=False, rdn='ou=Services'):
+ super(ServiceAccounts, self).__init__(instance, batch)
+ self._objectclasses = [
+ 'netscapeServer',
+ ]
+ self._filterattrs = [RDN]
+ self._childobject = ServiceAccount
+ self._basedn = '{},{}'.format(rdn, basedn)
+
diff --git a/src/lib389/lib389/idm/user.py b/src/lib389/lib389/idm/user.py
new file mode 100644
index 0000000..ee4e08d
--- /dev/null
+++ b/src/lib389/lib389/idm/user.py
@@ -0,0 +1,85 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, William Brown <william at blackhats.net.au>
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389._mapped_object import DSLdapObjects
+# Account derives DSLdapObject - it gives us the lock / unlock functions.
+from lib389.idm.account import Account
+from lib389.utils import ds_is_older
+
+MUST_ATTRIBUTES = [
+ 'uid',
+ 'cn',
+ 'sn',
+ 'uidNumber',
+ 'gidNumber',
+ 'homeDirectory',
+]
+RDN = 'uid'
+
+TEST_USER_PROPERTIES = {
+ 'uid': 'testuser',
+ 'cn' : 'testuser',
+ 'sn' : 'user',
+ 'uidNumber' : '1000',
+ 'gidNumber' : '2000',
+ 'homeDirectory' : '/home/testuser'
+}
+
+
+class UserAccount(Account):
+ def __init__(self, instance, dn=None, batch=False):
+ super(UserAccount, self).__init__(instance, dn, batch)
+ self._rdn_attribute = RDN
+ # Can I generate these from schema?
+ self._must_attributes = MUST_ATTRIBUTES
+ self._create_objectclasses = [
+ 'top',
+ 'account',
+ 'posixaccount',
+ # inetOrgPerson allows userCertificate
+ 'inetOrgPerson',
+ 'organizationalPerson',
+ # This may not always work at sites?
+ # Can we get this into core?
+ # 'ldapPublicKey',
+ ]
+ if ds_is_older('1.3.7'):
+ self._create_objectclasses.append('inetUser')
+ else:
+ self._create_objectclasses.append('nsMemberOf')
+ user_compare_exclude = [
+ 'nsUniqueId',
+ 'modifyTimestamp',
+ 'createTimestamp',
+ 'entrydn'
+ ]
+ self._compare_exclude = self._compare_exclude + user_compare_exclude
+ self._protected = False
+
+ def _validate(self, rdn, properties, basedn):
+ if 'ntUserDomainId' in properties and 'ntUser' not in
self._create_objectclasses:
+ self._create_objectclasses.append('ntUser')
+
+ return super(UserAccount, self)._validate(rdn, properties, basedn)
+
+ # Add a set password function....
+ # Can't I actually just set, and it will hash?
+
+class UserAccounts(DSLdapObjects):
+ def __init__(self, instance, basedn, batch=False, rdn='ou=People'):
+ super(UserAccounts, self).__init__(instance, batch)
+ self._objectclasses = [
+ 'account',
+ 'posixaccount',
+ 'inetOrgPerson',
+ 'organizationalPerson',
+ ]
+ self._filterattrs = [RDN]
+ self._childobject = UserAccount
+ self._basedn = '{},{}'.format(rdn, basedn)
+
diff --git a/src/lib389/lib389/index.py b/src/lib389/lib389/index.py
new file mode 100644
index 0000000..cf66368
--- /dev/null
+++ b/src/lib389/lib389/index.py
@@ -0,0 +1,124 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+
+import sys
+from lib389._constants import *
+from lib389.properties import *
+from lib389 import Entry
+from lib389.utils import ensure_str, ensure_bytes
+
+from lib389._mapped_object import DSLdapObjects, DSLdapObject
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+if MAJOR >= 3 or (MAJOR == 2 and MINOR >= 7):
+ from ldap.controls.readentry import PostReadControl
+
+DEFAULT_INDEX_DN = "cn=default indexes,%s" % DN_CONFIG_LDBM
+
+class Index(DSLdapObject):
+ def __init__(self, instance, dn=None, batch=False):
+ super(Index, self).__init__(instance, dn, batch)
+ self._rdn_attribute = 'cn'
+ self._must_attributes = ['cn', 'nsSystemIndex',
'nsIndexType']
+ self._create_objectclasses = ['top', 'nsIndex']
+ self._protected = False
+ self._lint_functions = []
+
+class Indexes(DSLdapObjects):
+ def __init__(self, instance, basedn=DEFAULT_INDEX_DN, batch=False):
+ super(Indexes, self).__init__(instance=instance, batch=batch)
+ self._objectclasses = ['nsIndex']
+ self._filterattrs = ['cn']
+ self._childobject = Index
+ self._basedn = basedn
+
+class IndexLegacy(object):
+
+ def __init__(self, conn):
+ """@param conn - a DirSrv instance"""
+ self.conn = conn
+ self.log = conn.log
+
+ def delete_all(self, benamebase):
+ benamebase = ensure_str(benamebase)
+ dn = "cn=index,cn=" + benamebase + "," + DN_LDBM
+
+ # delete each defined index
+ self.conn.delete_branch_s(dn, ldap.SCOPE_ONELEVEL)
+
+ # Then delete the top index entry
+ self.log.debug("Delete head index entry %s" % (dn))
+ self.conn.delete_s(dn)
+
+ def create(self, suffix=None, be_name=None, attr=None, args=None):
+ if not suffix and not be_name:
+ raise ValueError("suffix/backend name is mandatory parameter")
+
+ if not attr:
+ raise ValueError("attr is mandatory parameter")
+
+ indexTypes = args.get(INDEX_TYPE, None)
+ matchingRules = args.get(INDEX_MATCHING_RULE, None)
+
+ return self.addIndex(suffix, be_name, attr, indexTypes=indexTypes,
+ matchingRules=matchingRules)
+
+ def addIndex(self, suffix, be_name, attr, indexTypes, matchingRules,
+ postReadCtrl=None):
+ """Specify the suffix (should contain 1 local database backend),
+ the name of the attribute to index, and the types of indexes
+ to create e.g. "pres", "eq", "sub"
+ """
+ msg_id = None
+ if be_name:
+ dn = ('cn=%s,cn=index,cn=%s,cn=ldbm database,cn=plugins,cn=config'
+ % (attr, be_name))
+ else:
+ entries_backend = self.conn.backend.list(suffix=suffix)
+ # assume 1 local backend
+ dn = "cn=%s,cn=index,%s" % (attr, entries_backend[0].dn)
+
+ if postReadCtrl:
+ add_record = [('nsSystemIndex', ['false']),
+ ('cn', [attr]),
+ ('objectclass', ['top', 'nsindex']),
+ ('nsIndexType', indexTypes)]
+ if matchingRules:
+ add_record.append(('nsMatchingRule', matchingRules))
+
+ else:
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top', 'nsIndex')
+ entry.setValues('cn', attr)
+ entry.setValues('nsSystemIndex', "false")
+ entry.setValues('nsIndexType', indexTypes)
+ if matchingRules:
+ entry.setValues('nsMatchingRule', matchingRules)
+
+ if MAJOR >= 3 or (MAJOR == 2 and MINOR >= 7):
+ try:
+ if postReadCtrl:
+ pr = PostReadControl(criticality=True, attrList=['*'])
+ msg_id = self.conn.add_ext(dn, add_record, serverctrls=[pr])
+ else:
+ self.conn.add_s(entry)
+ except ldap.LDAPError as e:
+ raise e
+
+ return msg_id
+
+ def modIndex(self, suffix, attr, mod):
+ """just a wrapper around a plain old ldap modify, but will
+ find the correct index entry based on the suffix and attribute"""
+ entries_backend = self.conn.backend.list(suffix=suffix)
+ # assume 1 local backend
+ dn = "cn=%s,cn=index,%s" % (attr, entries_backend[0].dn)
+ self.conn.modify_s(dn, mod)
diff --git a/src/lib389/lib389/instance/__init__.py
b/src/lib389/lib389/instance/__init__.py
new file mode 100644
index 0000000..d664b4e
--- /dev/null
+++ b/src/lib389/lib389/instance/__init__.py
@@ -0,0 +1,7 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
diff --git a/src/lib389/lib389/instance/options.py
b/src/lib389/lib389/instance/options.py
new file mode 100644
index 0000000..f057fe0
--- /dev/null
+++ b/src/lib389/lib389/instance/options.py
@@ -0,0 +1,301 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import socket
+import sys
+import os
+from lib389.paths import Paths
+from lib389._constants import INSTALL_LATEST_CONFIG
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+if MAJOR >= 3:
+ import configparser
+else:
+ import ConfigParser as configparser
+
+format_keys = [
+ 'prefix',
+ 'bin_dir',
+ 'sbin_dir',
+ 'sysconf_dir',
+ 'data_dir',
+ 'local_state_dir',
+ 'lib_dir',
+ 'cert_dir',
+ 'config_dir',
+ 'inst_dir',
+ 'backup_dir',
+ 'db_dir',
+ 'ldif_dir',
+ 'lock_dir',
+ 'log_dir',
+ 'run_dir',
+ 'schema_dir',
+ 'tmp_dir',
+]
+
+ds_paths = Paths()
+
+class Options2(object):
+ # This stores the base options in a self._options dict.
+ # It provides a number of options for:
+ # - dict overlay
+ # - parsing the config parser types.
+ def __init__(self, log):
+ # 'key' : (default, helptext, valid_func )
+ self._options = {} # this takes the default
+ self._type = {} # Lists the type the item should be.
+ self._helptext = {} # help text for the option, MANDATORY.
+ self._example_comment = {} # If this is a commented value in the example.
+ self._valid_func = {} # options verification function.
+ self._section = None
+ self.log = log
+
+ def parse_inf_config(self, config):
+ v = None
+ for k in self._options.keys():
+ try:
+ if self._type[k] == int:
+ v = config.getint(self._section, k)
+ elif self._type[k] == bool:
+ v = config.getboolean(self._section, k)
+ elif self._type[k] == str:
+ v = config.get(self._section, k)
+ # How does this handle wrong types?
+ except ValueError:
+ # Should we raise an assertion error?
+ # No, the sectons don't exist, continue
+ self.log.debug('%s:%s not in inf, or incorrect type, using
default' % (self._section, k))
+ continue
+ except configparser.NoOptionError:
+ self.log.debug('%s:%s not in inf, or incorrect type, using
default' % (self._section, k))
+ continue
+ self._options[k] = v
+
+ def set(self, option, value):
+ self._options[option] = value
+
+ def verify(self):
+ pass
+
+ def collect(self):
+ return self._options
+
+ def collect_help(self):
+ helptext = "[%s]\n" % self._section
+ for k in sorted(self._options.keys()):
+ helptext += "# %s: %s\n" % (k, self._helptext[k])
+ helptext += "# type: %s\n" % (self._type[k].__name__)
+ if self._example_comment[k]:
+ helptext += "; %s = %s\n\n" % (k, self._options[k])
+ else:
+ helptext += "%s = %s\n\n" % (k, self._options[k])
+ return helptext
+
+#
+# Base, example dicts of the general, backend (userRoot) options.
+#
+
+class General2Base(Options2):
+ def __init__(self, log):
+ super(General2Base, self).__init__(log)
+ self._section = 'general'
+
+ self._options['config_version'] = 2
+ self._type['config_version'] = int
+ self._helptext['config_version'] = "The format version of the inf
answer file."
+ self._example_comment['config_version'] = False
+
+ self._options['full_machine_name'] = socket.gethostname()
+ self._type['full_machine_name'] = str
+ self._helptext['full_machine_name'] = "The fully qualified hostname
of this system."
+ self._example_comment['full_machine_name'] = False
+
+ self._options['strict_host_checking'] = True
+ self._type['strict_host_checking'] = bool
+ self._helptext['strict_host_checking'] = "If true, will validate
forward and reverse dns names for full_machine_name"
+ self._example_comment['strict_host_checking'] = True
+
+ self._options['selinux'] = True
+ self._type['selinux'] = bool
+ self._helptext['selinux'] = "Enable SELinux detection and
integration. Normally, this should always be True, and will correctly detect when SELinux
is not present."
+ self._example_comment['selinux'] = True
+
+ self._options['systemd'] = ds_paths.with_systemd
+ self._type['systemd'] = bool
+ self._helptext['systemd'] = "Enable systemd platform features. This
is autodetected on your platform. You should not alter this value without good
reason."
+ self._example_comment['systemd'] = True
+
+ self._options['defaults'] = INSTALL_LATEST_CONFIG
+ self._type['defaults'] = str
+ self._helptext['defaults'] = "Set the configuration defaults
version. If set to %{LATEST}, always use the latest values available for the slapd
section. This allows pinning default values in cn=config to specific Directory Server
releases. This maps to our versions such as 1.3.5 -> 001003005. The leading 0's are
important.".format(LATEST=INSTALL_LATEST_CONFIG)
+ self._example_comment['defaults'] = True
+
+#
+# This module contains the base options and configs for Director Server
+# setup and install. This allows
+#
+
+class Slapd2Base(Options2):
+ def __init__(self, log):
+ super(Slapd2Base, self).__init__(log)
+ self._section = 'slapd'
+
+ self._options['instance_name'] = 'localhost'
+ self._type['instance_name'] = str
+ self._helptext['instance_name'] = "The name of the instance. Cannot
be changed post installation."
+ self._example_comment['instance_name'] = False
+
+ self._options['user'] = ds_paths.user
+ self._type['user'] = str
+ self._helptext['user'] = "The user account ns-slapd will drop
privileges to during operation."
+ self._example_comment['user'] = True
+
+ self._options['group'] = ds_paths.group
+ self._type['group'] = str
+ self._helptext['group'] = "The group ns-slapd will drop privilleges
to during operation."
+ self._example_comment['group'] = True
+
+ self._options['root_dn'] = ds_paths.root_dn
+ self._type['root_dn'] = str
+ self._helptext['root_dn'] = "The Distinquished Name of the
Administrator account. This is equivalent to root of your Directory Server."
+ self._example_comment['root_dn'] = True
+
+ self._options['root_password'] = 'directory manager password'
+ self._type['root_password'] = str
+ self._helptext['root_password'] = "The password for the root_dn
account. "
+ self._example_comment['root_password'] = False
+
+ self._options['prefix'] = ds_paths.prefix
+ self._type['prefix'] = str
+ self._helptext['prefix'] = "The filesystem prefix for all other
locations. Unless you are developing DS, you likely never need to set this. This value can
be reffered to in other fields with {prefix}, and can be set with the environment variable
PREFIX."
+ self._example_comment['prefix'] = True
+
+ self._options['port'] = 389
+ self._type['port'] = int
+ self._helptext['port'] = "The TCP port that Directory Server will
listen on for LDAP connections."
+ self._example_comment['port'] = True
+
+ self._options['secure_port'] = 636
+ self._type['secure_port'] = int
+ self._helptext['secure_port'] = "The TCP port that Directory Server
will listen on for TLS secured LDAP connections."
+ self._example_comment['secure_port'] = True
+
+ # In the future, make bin and sbin /usr/[s]bin, but we may need autotools
assistance from Ds
+ self._options['bin_dir'] = ds_paths.bin_dir
+ self._type['bin_dir'] = str
+ self._helptext['bin_dir'] = "The location Directory Server can find
binaries. You should not need to alter this value."
+ self._example_comment['bin_dir'] = True
+
+ self._options['sbin_dir'] = ds_paths.sbin_dir
+ self._type['sbin_dir'] = str
+ self._helptext['sbin_dir'] = "The location Directory Server can find
systemd administration binaries. You should not need to alter this value."
+ self._example_comment['sbin_dir'] = True
+
+ self._options['sysconf_dir'] = ds_paths.sysconf_dir
+ self._type['sysconf_dir'] = str
+ self._helptext['sysconf_dir'] = "The location of the system
configuration directory. You should not need to alter this value."
+ self._example_comment['sysconf_dir'] = True
+
+ self._options['initconfig_dir'] = ds_paths.initconfig_dir
+ self._type['initconfig_dir'] = str
+ self._helptext['initconfig_dir'] = "The location of the system rc
configuration directory. You should not need to alter this value."
+ self._example_comment['initconfig_dir'] = True
+
+ # In the future, make bin and sbin /usr/[s]bin, but we may need autotools
assistance from Ds
+ self._options['data_dir'] = ds_paths.data_dir
+ self._type['data_dir'] = str
+ self._helptext['data_dir'] = "The location of shared static data.
You should not need to alter this value."
+ self._example_comment['data_dir'] = True
+
+ self._options['local_state_dir'] = ds_paths.local_state_dir
+ self._type['local_state_dir'] = str
+ self._helptext['local_state_dir'] = "The location prefix to variable
data. You should not need to alter this value."
+ self._example_comment['local_state_dir'] = True
+
+ self._options['lib_dir'] = ds_paths.lib_dir
+ self._type['lib_dir'] = str
+ self._helptext['lib_dir'] = "The location to Directory Server shared
libraries. You should not need to alter this value."
+ self._example_comment['lib_dir'] = True
+
+ self._options['cert_dir'] = ds_paths.cert_dir
+ self._type['cert_dir'] = str
+ self._helptext['cert_dir'] = "The location where NSS will store
certificates."
+ self._example_comment['cert_dir'] = True
+
+ self._options['config_dir'] = ds_paths.config_dir
+ self._type['config_dir'] = str
+ self._helptext['config_dir'] = "The location where dse.ldif and
other configuration will be stored. You should not need to alter this value."
+ self._example_comment['config_dir'] = True
+
+ self._options['inst_dir'] = ds_paths.inst_dir
+ self._type['inst_dir'] = str
+ self._helptext['inst_dir'] = "The location of the Directory Server
databases, ldif and backups. You should not need to alter this value."
+ self._example_comment['inst_dir'] = True
+
+ self._options['backup_dir'] = ds_paths.backup_dir
+ self._type['backup_dir'] = str
+ self._helptext['backup_dir'] = "The location where Directory Server
will export and import backups from. You should not need to alter this value."
+ self._example_comment['backup_dir'] = True
+
+ self._options['db_dir'] = ds_paths.db_dir
+ self._type['db_dir'] = str
+ self._helptext['db_dir'] = "The location where Directory Server will
store databases. You should not need to alter this value."
+ self._example_comment['db_dir'] = True
+
+ self._options['ldif_dir'] = ds_paths.ldif_dir
+ self._type['ldif_dir'] = str
+ self._helptext['ldif_dir'] = "The location where Directory Server
will export and import ldif from. You should not need to alter this value."
+ self._example_comment['ldif_dir'] = True
+
+ self._options['lock_dir'] = ds_paths.lock_dir
+ self._type['lock_dir'] = str
+ self._helptext['lock_dir'] = "The location where Directory Server
will store lock files. You should not need to alter this value."
+ self._example_comment['lock_dir'] = True
+
+ self._options['log_dir'] = ds_paths.log_dir
+ self._type['log_dir'] = str
+ self._helptext['log_dir'] = "The location where Directory Server
will write log files. You should not need to alter this value."
+ self._example_comment['log_dir'] = True
+
+ self._options['run_dir'] = ds_paths.run_dir
+ self._type['run_dir'] = str
+ self._helptext['run_dir'] = "The location where Directory Server
will create pid files. You should not need to alter this value."
+ self._example_comment['run_dir'] = True
+
+ self._options['schema_dir'] = ds_paths.schema_dir
+ self._type['schema_dir'] = str
+ self._helptext['schema_dir'] = "The location where Directory Server
will store and write schema. You should not need to alter this value."
+ self._example_comment['schema_dir'] = True
+
+ self._options['tmp_dir'] = ds_paths.tmp_dir
+ self._type['tmp_dir'] = str
+ self._helptext['tmp_dir'] = "The location where Directory Server
will write temporary files. You should not need to alter this value."
+ self._example_comment['tmp_dir'] = True
+
+ def _format(self, d):
+ new_d = {}
+ ks = d.keys()
+ no_format_keys = set(ks) - set(format_keys)
+
+ for k in no_format_keys:
+ new_d[k] = d[k]
+ for k in format_keys:
+ # Will these be done in correct order?
+ if self._type[k] == str:
+ new_d[k] = d[k].format(**new_d)
+ else:
+ new_d[k] = d[k]
+ return new_d
+
+ def collect(self):
+ # This does the final format and return of options.
+ return self._format(self._options)
+
+
diff --git a/src/lib389/lib389/instance/remove.py b/src/lib389/lib389/instance/remove.py
new file mode 100644
index 0000000..689b5bd
--- /dev/null
+++ b/src/lib389/lib389/instance/remove.py
@@ -0,0 +1,54 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import os
+import shutil
+
+def remove_ds_instance(dirsrv):
+ """
+ This will delete the instance as it is define. This must be a local instance.
+ """
+ _log = dirsrv.log.getChild('remove_ds')
+ _log.debug("Removing instance %s" % dirsrv.serverid)
+ # Stop the instance (if running)
+ _log.debug("Stopping instance %s" % dirsrv.serverid)
+ dirsrv.stop()
+ # Copy all the paths we are about to tamp with
+ remove_paths = {}
+ remove_paths['backup_dir'] = dirsrv.ds_paths.backup_dir
+ remove_paths['cert_dir'] = dirsrv.ds_paths.cert_dir
+ remove_paths['config_dir'] = dirsrv.ds_paths.config_dir
+ remove_paths['db_dir'] = dirsrv.ds_paths.db_dir
+ remove_paths['ldif_dir'] = dirsrv.ds_paths.ldif_dir
+ remove_paths['lock_dir'] = dirsrv.ds_paths.lock_dir
+ remove_paths['log_dir'] = dirsrv.ds_paths.log_dir
+ remove_paths['run_dir'] = dirsrv.ds_paths.run_dir
+
+ marker_path = "%s/sysconfig/dirsrv-%s" % (dirsrv.ds_paths.sysconf_dir,
dirsrv.serverid)
+
+ # Check the marker exists. If it *does not* warn about this, and say that to
+ # force removal you should touch this file.
+
+ _log.debug("Checking for instance marker at %s" % marker_path)
+ assert os.path.exists(marker_path)
+
+ # Remove these paths:
+ # for path in ('backup_dir', 'cert_dir', 'config_dir',
'db_dir',
+ # 'ldif_dir', 'lock_dir', 'log_dir',
'run_dir'):
+ for path_k in remove_paths:
+ if os.path.exists(remove_paths[path_k]):
+ _log.debug("Removing %s" % remove_paths[path_k])
+ shutil.rmtree(remove_paths[path_k])
+
+ # Finally remove the sysconfig marker.
+ os.remove(marker_path)
+ _log.debug("Removing %s" % marker_path)
+
+ # Done!
+ _log.debug("Complete")
+
diff --git a/src/lib389/lib389/instance/setup.py b/src/lib389/lib389/instance/setup.py
new file mode 100644
index 0000000..cc94b48
--- /dev/null
+++ b/src/lib389/lib389/instance/setup.py
@@ -0,0 +1,454 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import os
+import ldap
+import shutil
+import sys
+import pwd
+import grp
+import re
+import socket
+import subprocess
+
+from lib389 import _ds_shutil_copytree
+from lib389._constants import *
+from lib389.properties import *
+from lib389.passwd import password_hash, password_generate
+
+from lib389.configurations import get_config
+
+from lib389.instance.options import General2Base, Slapd2Base
+
+# The poc backend api
+from lib389.backend import Backends
+from lib389.utils import (
+ is_a_dn,
+ ensure_bytes,
+ ensure_str,
+ socket_check_open,)
+
+try:
+ # There are too many issues with this on EL7
+ # Out of the box, it's just outright broken ...
+ import six.moves.urllib.request
+ import six.moves.urllib.parse
+ import six.moves.urllib.error
+ import six
+except ImportError:
+ pass
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+if MAJOR >= 3:
+ import configparser
+else:
+ import ConfigParser as configparser
+
+
+class SetupDs(object):
+ """
+ Implements the Directory Server installer.
+
+ This maybe subclassed, and a number of well known steps will be called.
+ This allows the inf to be shared, and for other installers to work in lock
+ step with this.
+
+ If you are subclassing you want to derive:
+
+ _validate_config_2(self, config):
+ _prepare(self, extra):
+ _install(self, extra):
+
+ If you are calling this from an INF, you can pass the config in
+ _validate_config, then stash the result into self.extra
+
+ If you have anything you need passed to your install helpers, this can
+ be given in create_from_args(extra) if you are calling as an api.
+
+ If you use create_from_inf, self.extra is passed to create_from_args for
+ you. You only need to over-load the three methods above.
+
+ A logging interface is provided to self.log that you should call.
+ """
+
+ def __init__(self, verbose=False, dryrun=False, log=None, containerised=False):
+ self.verbose = verbose
+ self.extra = None
+ self.dryrun = dryrun
+ # Expose the logger to our children.
+ self.log = log.getChild('SetupDs')
+ if self.verbose:
+ self.log.info('Running setup with verbose')
+ # This will indicate that start / stop / status should bypass systemd.
+ self.containerised = containerised
+
+ # Could be nicer if we did self._get_config_fallback_<type>?
+ def _get_config_fallback(self, config, group, attr, value, boolean=False,
num=False):
+ try:
+ if boolean:
+ return config.getboolean(group, attr)
+ elif num:
+ return config.getint(group, attr)
+ else:
+ return config.get(group, attr)
+ except ValueError:
+ return value
+ except configparser.NoOptionError:
+ self.log.info("%s not specified:setting to default - %s" % (attr,
value))
+ return value
+
+ def _validate_config_2(self, config):
+ pass
+
+ def _prepare(self, extra):
+ pass
+
+ def _install(self, extra):
+ pass
+
+ def _validate_ds_2_config(self, config):
+ assert config.has_section('slapd')
+ # Extract them in a way that create can understand.
+
+ general_options = General2Base(self.log)
+ general_options.parse_inf_config(config)
+ general_options.verify()
+ general = general_options.collect()
+
+ if self.verbose:
+ self.log.info("Configuration general %s" % general)
+
+ slapd_options = Slapd2Base(self.log)
+ slapd_options.parse_inf_config(config)
+ slapd_options.verify()
+ slapd = slapd_options.collect()
+
+ if self.verbose:
+ self.log.info("Configuration slapd %s" % slapd)
+
+ backends = []
+ for section in config.sections():
+ if section.startswith('backend-'):
+ be = {}
+ # TODO: Add the other BACKEND_ types
+ be[BACKEND_NAME] = section.replace('backend-', '')
+ be[BACKEND_SUFFIX] = config.get(section, 'suffix')
+ be[BACKEND_SAMPLE_ENTRIES] = config.get(section,
'sample_entries')
+ backends.append(be)
+
+ if self.verbose:
+ self.log.info("Configuration backends %s" % backends)
+
+ return (general, slapd, backends)
+
+ def _validate_ds_config(self, config):
+ # This will move to lib389 later.
+ # Check we have all the sections.
+ # Make sure we have needed keys.
+ assert(config.has_section('general'))
+ assert(config.has_option('general', 'config_version'))
+ assert(config.get('general', 'config_version') >=
'2')
+ if config.get('general', 'config_version') == '2':
+ # Call our child api to validate itself from the inf.
+ self._validate_config_2(config)
+ return self._validate_ds_2_config(config)
+ else:
+ self.log.info("Failed to validate configuration version.")
+ assert(False)
+
+ def create_from_inf(self, inf_path):
+ """
+ Will trigger a create from the settings stored in inf_path
+ """
+ # Get the inf file
+ if self.verbose:
+ self.log.info("Using inf from %s" % inf_path)
+ if not os.path.isfile(inf_path):
+ self.log.error("%s is not a valid file path" % inf_path)
+ return False
+ config = None
+ try:
+ config = configparser.SafeConfigParser()
+ config.read([inf_path])
+ except Exception as e:
+ self.log.error("Exception %s occured" % e)
+ return False
+
+ if self.verbose:
+ self.log.info("Configuration %s" % config.sections())
+
+ (general, slapd, backends) = self._validate_ds_config(config)
+
+ # Actually do the setup now.
+ self.create_from_args(general, slapd, backends, self.extra)
+
+ return True
+
+ def _prepare_ds(self, general, slapd, backends):
+
+ assert(general['defaults'] is not None)
+ if self.verbose:
+ self.log.info("PASSED: using config settings %s" %
general['defaults'])
+ # Validate our arguments.
+ assert(slapd['user'] is not None)
+ # check the user exists
+ assert(pwd.getpwnam(slapd['user']))
+ slapd['user_uid'] = pwd.getpwnam(slapd['user']).pw_uid
+ assert(slapd['group'] is not None)
+ assert(grp.getgrnam(slapd['group']))
+ slapd['group_gid'] = grp.getgrnam(slapd['group']).gr_gid
+ # check this group exists
+ # Check that we are running as this user / group, or that we are root.
+ assert(os.geteuid() == 0 or getpass.getuser() == slapd['user'])
+
+ if self.verbose:
+ self.log.info("PASSED: user / group checking")
+
+ assert(general['full_machine_name'] is not None)
+ assert(general['strict_host_checking'] is not None)
+ if general['strict_host_checking'] is True:
+ # Check it resolves with dns
+ assert(socket.gethostbyname(general['full_machine_name']))
+ if self.verbose:
+ self.log.info("PASSED: Hostname strict checking")
+
+ assert(slapd['prefix'] is not None)
+ if (slapd['prefix'] != ""):
+ assert(os.path.exists(slapd['prefix']))
+ if self.verbose:
+ self.log.info("PASSED: prefix checking")
+
+ # We need to know the prefix before we can do the instance checks
+ assert(slapd['instance_name'] is not None)
+ # Check if the instance exists or not.
+ # Should I move this import? I think this prevents some recursion
+ from lib389 import DirSrv
+ ds = DirSrv(verbose=self.verbose)
+ ds.containerised = self.containerised
+ ds.prefix = slapd['prefix']
+ insts = ds.list(serverid=slapd['instance_name'])
+ assert(len(insts) == 0)
+
+ if self.verbose:
+ self.log.info("PASSED: instance checking")
+
+ assert(slapd['root_dn'] is not None)
+ # Assert this is a valid DN
+ assert(is_a_dn(slapd['root_dn']))
+ assert(slapd['root_password'] is not None)
+ # Check if pre-hashed or not.
+ # !!!!!!!!!!!!!!
+
+ # Right now, the way that rootpw works on ns-slapd works, it force hashes the pw
+ # see
https://fedorahosted.org/389/ticket/48859
+ if not re.match('^\{[A-Z0-9]+\}.*$', slapd['root_password']):
+ # We need to hash it. Call pwdhash-bin.
+ # slapd['root_password'] =
password_hash(slapd['root_password'], prefix=slapd['prefix'])
+ pass
+ else:
+ pass
+
+ # Create a random string
+ # Hash it.
+ # This will be our temporary rootdn password so that we can do
+ # live mods and setup rather than static ldif manipulations.
+ self._raw_secure_password = password_generate()
+ self._secure_password = password_hash(self._raw_secure_password,
bin_dir=slapd['bin_dir'])
+
+ if self.verbose:
+ self.log.info("INFO: temp root password set to %s" %
self._raw_secure_password)
+ self.log.info("PASSED: root user checking")
+
+ assert(slapd['port'] is not None)
+ assert(socket_check_open('::1', slapd['port']) is False)
+ ## This causes some problems in tests :(
+ # assert(slapd['secure_port'] is not None)
+ if slapd['secure_port'] is not None:
+ assert(socket_check_open('::1', slapd['secure_port']) is
False)
+ if self.verbose:
+ self.log.info("PASSED: network avaliability checking")
+
+ # Make assertions of the paths?
+
+ # Make assertions of the backends?
+
+ def create_from_args(self, general, slapd, backends=[], extra=None):
+ """
+ Actually does the setup. this is what you want to call as an api.
+ """
+ # Check we have privs to run
+
+ if self.verbose:
+ self.log.info("READY: preparing installation for %s" %
slapd['instance_name'])
+ self._prepare_ds(general, slapd, backends)
+ # Call our child api to prepare itself.
+ self._prepare(extra)
+
+ if self.verbose:
+ self.log.info("READY: beginning installation for %s" %
slapd['instance_name'])
+
+ if self.dryrun:
+ self.log.info("NOOP: dry run requested")
+ else:
+ # Actually trigger the installation.
+ self._install_ds(general, slapd, backends)
+ # Call the child api to do anything it needs.
+ self._install(extra)
+ self.log.info("FINISH: completed installation for %s" %
slapd['instance_name'])
+
+ def _install_ds(self, general, slapd, backends):
+ """
+ Actually install the Ds from the dicts provided.
+
+ You should never call this directly, as it bypasses assertions.
+ """
+ # register the instance to /etc/sysconfig
+ # We do this first so that we can trick remove-ds.pl if needed.
+ # There may be a way to create this from template like the dse.ldif ...
+ initconfig = ""
+ with open("%s/dirsrv/config/template-initconfig" %
slapd['sysconf_dir']) as template_init:
+ for line in template_init.readlines():
+ initconfig += line.replace('{{', '{',
1).replace('}}', '}', 1).replace('-', '_')
+ with open("%s/sysconfig/dirsrv-%s" % (slapd['sysconf_dir'],
slapd['instance_name']), 'w') as f:
+ f.write(initconfig.format(
+ SERVER_DIR=slapd['lib_dir'],
+ SERVERBIN_DIR=slapd['sbin_dir'],
+ CONFIG_DIR=slapd['config_dir'],
+ INST_DIR=slapd['inst_dir'],
+ RUN_DIR=slapd['run_dir'],
+ DS_ROOT='',
+ PRODUCT_NAME='slapd',
+ ))
+
+ # Create all the needed paths
+ # we should only need to make bak_dir, cert_dir, config_dir, db_dir, ldif_dir,
lock_dir, log_dir, run_dir? schema_dir,
+ for path in ('backup_dir', 'cert_dir', 'config_dir',
'db_dir', 'ldif_dir', 'lock_dir', 'log_dir',
'run_dir'):
+ if self.verbose:
+ self.log.info("ACTION: creating %s" % slapd[path])
+ try:
+ os.makedirs(slapd[path], mode=0o775)
+ except OSError:
+ pass
+ os.chown(slapd[path], slapd['user_uid'], slapd['group_gid'])
+ ### Warning! We need to down the directory under db too for .restore to work.
+ # See dblayer.c for more!
+ db_parent = os.path.join(slapd['db_dir'], '..')
+ os.chown(db_parent, slapd['user_uid'], slapd['group_gid'])
+
+ # Copy correct data to the paths.
+ # Copy in the schema
+ # This is a little fragile, make it better.
+ # It won't matter when we move schema to usr anyway ...
+
+ _ds_shutil_copytree(os.path.join(slapd['sysconf_dir'],
'dirsrv/schema'), slapd['schema_dir'])
+ os.chown(slapd['schema_dir'], slapd['user_uid'],
slapd['group_gid'])
+
+ # Copy in the collation
+ srcfile = os.path.join(slapd['sysconf_dir'],
'dirsrv/config/slapd-collations.conf')
+ dstfile = os.path.join(slapd['config_dir'],
'slapd-collations.conf')
+ shutil.copy2(srcfile, dstfile)
+ os.chown(slapd['schema_dir'], slapd['user_uid'],
slapd['group_gid'])
+
+ # If we are on the correct platform settings, systemd
+ if general['systemd'] and not self.containerised:
+ # Should create the symlink we need, but without starting it.
+ subprocess.check_call(["/usr/bin/systemctl",
+ "enable",
+ "dirsrv@%s" %
slapd['instance_name']])
+ # Else we need to detect other init scripts?
+
+ # Bind sockets to our type?
+
+ # Create certdb in sysconfidir
+ if self.verbose:
+ self.log.info("ACTION: Creating certificate database is %s" %
slapd['cert_dir'])
+
+ # Create dse.ldif with a temporary root password.
+ # The template is in slapd['data_dir']/dirsrv/data/template-dse.ldif
+ # Variables are done with %KEY%.
+ # You could cheat and read it in, do a replace of % to { and } then use format?
+ if self.verbose:
+ self.log.info("ACTION: Creating dse.ldif")
+ dse = ""
+ with open(os.path.join(slapd['data_dir'], 'dirsrv',
'data', 'template-dse.ldif')) as template_dse:
+ for line in template_dse.readlines():
+ dse += line.replace('%', '{', 1).replace('%',
'}', 1)
+
+ with open(os.path.join(slapd['config_dir'], 'dse.ldif'),
'w') as file_dse:
+ file_dse.write(dse.format(
+ schema_dir=slapd['schema_dir'],
+ lock_dir=slapd['lock_dir'],
+ tmp_dir=slapd['tmp_dir'],
+ cert_dir=slapd['cert_dir'],
+ ldif_dir=slapd['ldif_dir'],
+ bak_dir=slapd['backup_dir'],
+ run_dir=slapd['run_dir'],
+ inst_dir="",
+ log_dir=slapd['log_dir'],
+ fqdn=general['full_machine_name'],
+ ds_port=slapd['port'],
+ ds_user=slapd['user'],
+ rootdn=slapd['root_dn'],
+ # ds_passwd=slapd['root_password'],
+ ds_passwd=self._secure_password, # We set our own password here, so we
can connect and mod.
+ ds_suffix='',
+ config_dir=slapd['config_dir'],
+ db_dir=slapd['db_dir'],
+ ))
+
+ # open the connection to the instance.
+
+ # Should I move this import? I think this prevents some recursion
+ from lib389 import DirSrv
+ ds_instance = DirSrv(self.verbose)
+ ds_instance.containerised = self.containerised
+ args = {
+ SER_PORT: slapd['port'],
+ SER_SERVERID_PROP: slapd['instance_name'],
+ SER_ROOT_DN: slapd['root_dn'],
+ SER_ROOT_PW: self._raw_secure_password,
+ SER_DEPLOYED_DIR: slapd['prefix']
+ }
+
+ ds_instance.allocate(args)
+ # Does this work?
+ assert(ds_instance.exists())
+ # Create the nssdb
+ assert(ds_instance.nss_ssl.reinit())
+ # Do we want to selfsign a CA and cert?
+
+ ## LAST CHANCE, FIX PERMISSIONS.
+ # Selinux fixups?
+ # Restorecon of paths?
+
+ # Start the server
+ ds_instance.start(timeout=60)
+ ds_instance.open()
+
+ # Create the configs related to this version.
+ base_config = get_config(general['defaults'])
+ base_config_inst = base_config(ds_instance)
+ base_config_inst.apply_config(install=True)
+
+ # Create the backends as listed
+ # Load example data if needed.
+ for backend in backends:
+ ds_instance.backends.create(properties=backend)
+
+ # Make changes using the temp root
+ # Change the root password finally
+
+ # Complete.
+ ds_instance.config.set('nsslapd-rootpw',
+ ensure_str(slapd['root_password']))
+
+ # In a container build we need to stop DirSrv at the end
+ if self.containerised:
+ ds_instance.stop()
+
diff --git a/src/lib389/lib389/ldclt.py b/src/lib389/lib389/ldclt.py
new file mode 100644
index 0000000..133f460
--- /dev/null
+++ b/src/lib389/lib389/ldclt.py
@@ -0,0 +1,130 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import subprocess
+
+"""
+This class will allow general usage of ldclt.
+
+It's not meant to expose all the functions. Just use ldclt for that.
+
+It's meant to expose general use cases for tests in 389.
+
+Calling this on a production DS instance is likely a fast way to MESS THINGS UP.
+
+"""
+
+
+class Ldclt(object):
+ def __init__(self, ds):
+ self.ds = ds
+ self.verbose = self.ds.verbose
+ self.log = self.ds.log
+
+ def create_users(self, subtree, min=1000, max=9999, template=None):
+ """
+ Creates users as user<min through max>. Password will be set to
+
+ password<number>
+
+ This will automatically work with the bind loadtest.
+ """
+ # Should we check max > min?
+ # Create the template file given the data.
+ if template is None:
+ template = """
+objectClass: top
+objectclass: person
+objectClass: organizationalPerson
+objectClass: inetorgperson
+objectClass: posixAccount
+objectClass: shadowAccount
+sn: user[A]
+cn: user[A]
+givenName: user[A]
+description: description [A]
+userPassword: user[A]
+mail: user[A](a)example.com
+uidNumber: 1[A]
+gidNumber: 2[A]
+shadowMin: 0
+shadowMax: 99999
+shadowInactive: 30
+shadowWarning: 7
+homeDirectory: /home/user[A]
+loginShell: /bin/false
+"""
+ with open('/tmp/ldclt_template_lib389.ldif', 'wb') as f:
+ f.write(template)
+ # call ldclt with the current rootdn and rootpass
+ digits = len('%s' % max)
+
+ cmd = [
+ '%s/bin/ldclt' % self.ds.prefix,
+ '-h',
+ self.ds.host,
+ '-p',
+ '%s' % self.ds.port,
+ '-D',
+ self.ds.binddn,
+ '-w',
+ self.ds.bindpw,
+ '-b',
+ subtree,
+ '-e',
+ 'add,commoncounter',
+ '-e',
+
"object=/tmp/ldclt_template_lib389.ldif,rdn=uid:user[A=INCRNNOLOOP(%s;%s;%s)]" %
(min, max, digits),
+ ]
+ result = None
+ if self.verbose:
+ self.log.info("ldclt begining user create ...")
+ self.log.info(' '.join(cmd))
+ try:
+ result = subprocess.check_output(cmd)
+ # If verbose, capture / log the output.
+ except subprocess.CalledProcessError as e:
+ print(' '.join(cmd))
+ print(result)
+ raise(e)
+ if self.verbose:
+ self.log.info(result)
+
+ def bind_loadtest(self, subtree, min=1000, max=9999, rounds=3):
+ # The bind users will be uid=userXXXX
+ digits = len('%s' % max)
+ cmd = [
+ '%s/bin/ldclt' % self.ds.prefix,
+ '-h',
+ self.ds.host,
+ '-p',
+ '%s' % self.ds.port,
+ '-N',
+ '%s' % rounds,
+ '-D',
+ 'uid=user%s,%s' % ('X' * digits, subtree),
+ '-w',
+ 'user%s' % ('X' * digits),
+ '-e',
+ "randombinddn,randombinddnlow=%s,randombinddnhigh=%s" % (min,
max),
+ '-e',
+ 'bindonly',
+ ]
+ result = None
+ if self.verbose:
+ self.log.info("ldclt loadtest ...")
+ self.log.info(' '.join(cmd))
+ try:
+ result = subprocess.check_output(cmd)
+ # If verbose, capture / log the output.
+ except subprocess.CalledProcessError as e:
+ print(' '.join(cmd))
+ print(result)
+ raise(e)
+ if self.verbose:
+ self.log.info(result)
diff --git a/src/lib389/lib389/lint.py b/src/lib389/lib389/lint.py
new file mode 100644
index 0000000..8c4b4de
--- /dev/null
+++ b/src/lib389/lib389/lint.py
@@ -0,0 +1,131 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# A set of constants defining the lint errors we can return to a caller.
+# as well as some functions to help process them.
+
+
+DSBLE0001 = {
+ 'dsle': 'DSBLE0001',
+ 'severity': 'MEDIUM',
+ 'items' : [],
+ 'detail' : """
+This backend may be missing the correct mapping tree references. Mapping Trees allow
+the directory server to determine which backend an operation is routed to in the
+abscence of other information. This is extremely important for correct functioning
+of LDAP ADD for example.
+
+A correct Mapping tree for this backend must contain the suffix name, the database name
+and be a backend type. IE:
+
+cn=o3Dexample,cn=mapping tree,cn=config
+cn: o=example
+nsslapd-backend: userRoot
+nsslapd-state: backend
+objectClass: top
+objectClass: extensibleObject
+objectClass: nsMappingTree
+
+ """,
+ 'fix' : """
+Either you need to create the mapping tree, or you need to repair the related
+mapping tree. You will need to do this by hand by editing cn=config, or stopping
+the instance and editing dse.ldif.
+ """
+}
+
+DSCLE0001 = {
+ 'dsle' : 'DSCLE0001',
+ 'severity' : 'LOW',
+ 'items': ['cn=config', ],
+ 'detail' : """
+nsslapd-logging-hr-timestamps-enabled changes the log format in directory server from
+
+[07/Jun/2017:17:15:58 +1000]
+
+to
+
+[07/Jun/2017:17:15:58.716117312 +1000]
+
+This actually provides a performance improvement. Additionally, this setting will be
+removed in a future release.
+ """,
+ 'fix' : """
+Set nsslapd-logging-hr-timestamps-enabled to on.
+ """
+}
+
+DSCLE0002 = {
+ 'dsle': 'DSCLE0002',
+ 'severity': 'HIGH',
+ 'items' : ['cn=config', ],
+ 'detail' : """
+Password storage schemes in Directory Server define how passwords are hashed via a
+one-way mathematical function for storage. Knowing the hash it is difficult to gain
+the input, but knowing the input you can easily compare the hash.
+
+Many hashes are well known for cryptograhpic verification properties, but are
+designed to be *fast* to validate. This is the opposite of what we desire for password
+storage. In the unlikely event of a disclosure, you want hashes to be *difficult* to
+verify, as this adds a cost of work to an attacker.
+
+In Directory Server, we offer one hash suitable for this (PBKDF2_SHA256) and one hash
+for "legacy" support (SSHA512).
+
+Your configuration does not use these for password storage or the root password storage
+scheme.
+ """,
+ 'fix': """
+Perform a configuration reset of the values:
+
+passwordStorageScheme
+nsslapd-rootpwstoragescheme
+
+IE, stop Directory Server, and in dse.ldif delete these two lines. When Directory Server
+is started, they will use the server provided defaults that are secure.
+ """
+}
+
+DSELE0001 = {
+ 'dsle': 'DSELE0001',
+ 'severity': 'MEDIUM',
+ 'items' : ['cn=encryption,cn=config', ],
+ 'detail': """
+This Directory Server may not be using strong TLS protocol versions. TLS1.0 is known to
+have a number of issues with the protocol. Please see:
+
+https://tools.ietf.org/html/rfc7457
+
+It is advised you set this value to the maximum possible.
+ """,
+ 'fix' : """
+set cn=encryption,cn=config sslVersionMin to a version greater than TLS1.0
+ """
+}
+
+DSRILE0001 = {
+ 'dsle': 'DSRLE0001',
+ 'severity': 'LOW',
+ 'items' : ['cn=referential integrity
postoperation,cn=plugins,cn=config', ],
+ 'detail': """
+The referential integrity plugin has an asynchronous processing mode. This is controlled
by the update-delay flag.
+
+When this value is 0, referential integrity plugin processes these changes inside of the
operation that modified the entry - ie these are synchronous.
+
+However, when this is > 0, these are performed asynchronously.
+
+This leads to only having refint enabled on one master in MMR to prevent replication
conflicts and loops.
+Additionally, because these are performed in the background these updates may cause
spurious update
+delays to your server by batching changes rather than smaller updates during sync
processing.
+
+We advise that you set this value to 0, and enable refint on all masters as it provides a
more predictable behaviour.
+ """,
+ 'fix' : """
+Set referint-update-delay to 0.
+ """
+}
diff --git a/src/lib389/lib389/mappingTree.py b/src/lib389/lib389/mappingTree.py
new file mode 100644
index 0000000..f319276
--- /dev/null
+++ b/src/lib389/lib389/mappingTree.py
@@ -0,0 +1,420 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+import ldap.dn
+import six
+
+from lib389._constants import *
+from lib389.properties import *
+from lib389.utils import suffixfilt, normalizeDN
+from lib389 import Entry
+from lib389.exceptions import NoSuchEntryError, InvalidArgumentError
+
+
+from lib389._mapped_object import DSLdapObjects, DSLdapObject
+
+
+class MappingTreeLegacy(object):
+ '''
+ classdocs
+ '''
+
+ proxied_methods = 'search_s getEntry'.split()
+
+ def __init__(self, conn):
+ """
+ @param conn - a DirSrv instance
+ """
+ self.conn = conn
+ self.log = conn.log
+
+ def __getattr__(self, name):
+ if name in MappingTree.proxied_methods:
+ from lib389 import DirSrv
+ return DirSrv.__getattr__(self.conn, name)
+
+ def list(self, suffix=None, bename=None):
+ '''
+ Returns a search result of the mapping tree entries with all their
+ attributes
+
+ If 'suffix'/'bename' are specified. It uses
'benamebase' first,
+ then 'suffix'.
+
+ If neither 'suffix' and 'bename' are specified, it returns
all
+ the mapping tree entries
+
+ @param suffix - suffix of the backend
+ @param benamebase - backend common name (e.g. 'userRoot')
+
+ @return mapping tree entries
+
+ @raise if search fails
+
+ '''
+ if bename:
+ filt = "(%s=%s)" % (MT_PROPNAME_TO_ATTRNAME[MT_BACKEND], bename)
+ elif suffix:
+ filt = "(%s=%s)" % (MT_PROPNAME_TO_ATTRNAME[MT_SUFFIX], suffix)
+ else:
+ filt = "(objectclass=%s)" % MT_OBJECTCLASS_VALUE
+
+ try:
+ ents = self.conn.search_s(DN_MAPPING_TREE, ldap.SCOPE_ONELEVEL,
+ filt)
+ for ent in ents:
+ self.log.debug('list: %r' % ent)
+ except:
+ raise
+
+ return ents
+
+ def create(self, suffix=None, bename=None, parent=None):
+ '''
+ Create a mapping tree entry (under "cn=mapping tree,cn=config"),
+ for the 'suffix' and that is stored in 'bename' backend.
+ 'bename' backend must exist before creating the mapping tree entry.
+
+ If a 'parent' is provided that means that we are creating a
+ sub-suffix mapping tree.
+
+ @param suffix - suffix mapped by this mapping tree entry. It will
+ be the common name ('cn') of the entry
+ @param benamebase - backend common name (e.g. 'userRoot')
+ @param parent - if provided is a parent suffix of 'suffix'
+
+ @return DN of the mapping tree entry
+
+ @raise ldap.NO_SUCH_OBJECT - if the backend entry or parent mapping
+ tree does not exist
+ ValueError - if missing a parameter,
+
+ '''
+ # Check suffix is provided
+ if not suffix:
+ raise ValueError("suffix is mandatory")
+ else:
+ nsuffix = normalizeDN(suffix)
+
+ # Check backend name is provided
+ if not bename:
+ raise ValueError("backend name is mandatory")
+
+ # Check that if the parent suffix is provided then
+ # it exists a mapping tree for it
+ if parent:
+ nparent = normalizeDN(parent)
+ filt = suffixfilt(parent)
+ try:
+ entry = self.conn.getEntry(DN_MAPPING_TREE, ldap.SCOPE_SUBTREE,
+ filt)
+ pass
+ except NoSuchEntryError:
+ raise ValueError("parent suffix has no mapping tree")
+ else:
+ nparent = ""
+
+ # Check if suffix exists, return
+ filt = suffixfilt(suffix)
+ try:
+ entry = self.conn.getEntry(DN_MAPPING_TREE, ldap.SCOPE_SUBTREE,
+ filt)
+ return entry
+ except ldap.NO_SUCH_OBJECT:
+ entry = None
+
+ #
+ # Now start the real work
+ #
+
+ # fix me when we can actually used escaped DNs
+ dn = ','.join(('cn="%s"' % nsuffix, DN_MAPPING_TREE))
+ entry = Entry(dn)
+ entry.update({
+ 'objectclass': ['top', 'extensibleObject',
MT_OBJECTCLASS_VALUE],
+ 'nsslapd-state': 'backend',
+ # the value in the dn has to be DN escaped
+ # internal code will add the quoted value - unquoted value is
+ # useful for searching.
+ MT_PROPNAME_TO_ATTRNAME[MT_SUFFIX]: nsuffix,
+ MT_PROPNAME_TO_ATTRNAME[MT_BACKEND]: bename
+ })
+
+ # possibly add the parent
+ if parent:
+ entry.setValues(MT_PROPNAME_TO_ATTRNAME[MT_PARENT_SUFFIX], nparent)
+
+ try:
+ self.log.debug("Creating entry: %s" % entry.dn)
+ self.log.info("Entry %r" % entry)
+ self.conn.add_s(entry)
+ except ldap.LDAPError as e:
+ raise ldap.LDAPError("Error adding suffix entry " + dn, e)
+
+ ret = self.conn._test_entry(dn, ldap.SCOPE_BASE)
+ return ret
+
+ def delete(self, suffix=None, bename=None, name=None):
+ '''
+ Delete a mapping tree entry (under "cn=mapping tree,cn=config"),
+ for the 'suffix' and that is stored in 'benamebase' backend.
+ 'benamebase' backend is not changed by the mapping tree deletion.
+
+ If 'name' is specified. It uses it to retrieve the mapping tree
+ to delete. Else if 'suffix'/'benamebase' are specified. It
uses
+ both to retrieve the mapping tree to delete
+
+ @param suffix - suffix mapped by this mapping tree entry. It is
+ the common name ('cn') of the entry
+ @param benamebase - backend common name (e.g. 'userRoot')
+ @param name - DN of the mapping tree entry
+
+ @return None
+
+ @raise ldap.NO_SUCH_OBJECT - the entry is not found
+ KeyError if 'name', 'suffix'
and
+ 'benamebase' are missing
+ UnwillingToPerformError - If the mapping tree has
+ subordinates
+ '''
+ if name:
+ filt = "(objectclass=%s)" % MT_OBJECTCLASS_VALUE
+ try:
+ ent = self.conn.getEntry(name, ldap.SCOPE_BASE, filt)
+ self.log.debug("delete: %s found by its DN" % ent.dn)
+ except NoSuchEntryError:
+ raise ldap.NO_SUCH_OBJECT("mapping tree DN not found: %s" %
+ name)
+ else:
+ filt = None
+
+ if suffix:
+ filt = suffixfilt(suffix)
+
+ if bename:
+ if filt:
+ filt = ("(&(%s=%s)%s)" %
+ (MT_PROPNAME_TO_ATTRNAME[MT_BACKEND],
+ bename,
+ filt))
+ else:
+ filt = ("(%s=%s)" %
+ (MT_PROPNAME_TO_ATTRNAME[MT_BACKEND], bename))
+
+ try:
+ ent = self.conn.getEntry(DN_MAPPING_TREE, ldap.SCOPE_ONELEVEL,
+ filt)
+ self.log.debug("delete: %s found by with %s" % (ent.dn, filt))
+ except NoSuchEntryError:
+ raise ldap.NO_SUCH_OBJECT("mapping tree DN not found: %s" %
+ name)
+
+ #
+ # At this point 'ent' contains the mapping tree entry to delete
+ #
+
+ # First Check there is no child (replica, replica agreements)
+ try:
+ ents = self.conn.search_s(ent.dn, ldap.SCOPE_SUBTREE,
+ "objectclass=*")
+ except:
+ raise
+ if len(ents) != 1:
+ for entry in ents:
+ self.log.warning("Error: it exists %s under %s" %
+ (entry.dn, ent.dn))
+ raise ldap.UNWILLING_TO_PERFORM(
+ "Unable to delete %s, it is not a leaf" % ent.dn)
+ else:
+ for entry in ents:
+ self.log.warning("Warning: %s (%s)" % (entry.dn, ent.dn))
+ self.conn.delete_s(ent.dn)
+
+ def getProperties(self, suffix=None, bename=None, name=None,
+ properties=None):
+ '''
+ Returns a dictionary of the requested properties.
+ If properties is missing, it returns all the properties.
+
+ The returned properties are those of the 'suffix' and that is
+ stored in 'benamebase' backend.
+
+ If 'name' is specified. It uses it to retrieve the mapping tree
+ to delete Else if 'suffix'/'benamebase' are specified. It
uses
+ both to retrieve the mapping tree to
+
+ If 'name', 'benamebase' and 'suffix' are missing it
raise an
+ exception
+
+ @param suffix - suffix mapped by this mapping tree entry.
+ It is the common name ('cn') of the entry
+ @param benamebase - backend common name (e.g. 'userRoot')
+ @param name - DN of the mapping tree entry
+ @param - properties - list of properties
+
+ @return - returns a dictionary of the properties
+
+ @raise ValueError - if some name of properties are not valid
+ KeyError - if some name of properties are not valid
+ ldap.NO_SUCH_OBJECT - if the mapping tree entry is not found
+ '''
+
+ if name:
+ filt = "(objectclass=%s)" % MT_OBJECTCLASS_VALUE
+
+ try:
+ ent = self.conn.getEntry(name, ldap.SCOPE_BASE, filt,
+ list(MT_PROPNAME_TO_ATTRNAME.values())
+ )
+ self.log.debug("delete: %s found by its DN" % ent.dn)
+ except NoSuchEntryError:
+ raise ldap.NO_SUCH_OBJECT("mapping tree DN not found: %s" %
+ name)
+ else:
+ filt = None
+
+ if suffix:
+ filt = suffixfilt(suffix)
+
+ if bename:
+ if filt:
+ filt = ("(&(%s=%s)%s)" %
+ (MT_PROPNAME_TO_ATTRNAME[MT_BACKEND],
+ bename, filt))
+ else:
+ filt = ("(%s=%s)" % (MT_PROPNAME_TO_ATTRNAME[MT_BACKEND],
+ bename))
+
+ try:
+ ent = self.conn.getEntry(DN_MAPPING_TREE, ldap.SCOPE_ONELEVEL,
+ filt,
+ list(MT_PROPNAME_TO_ATTRNAME.values())
+ )
+ self.log.debug("delete: %s found by with %s" % (ent.dn, filt))
+ except NoSuchEntryError:
+ raise ldap.NO_SUCH_OBJECT("mapping tree DN not found: %s" %
+ name)
+
+ result = {}
+ attrs = []
+ if properties:
+ #
+ # build the list of attributes we are looking for
+
+ for prop_name in properties:
+ prop_attr = MT_PROPNAME_TO_ATTRNAME[prop_name]
+ if not prop_attr:
+ raise ValueError("Improper property name: %s ", prop_name)
+ self.log.debug("Look for attr %s (property: %s)" %
+ (prop_attr, prop_name))
+ attrs.append(prop_attr)
+
+ # now look for each attribute from the MT entry
+ for attr in ent.getAttrs():
+ # given an attribute name retrieve the property name
+ props = [k for k, v in six.iteritems(MT_PROPNAME_TO_ATTRNAME)
+ if v.lower() == attr.lower()]
+
+ # If this attribute is present in the MT properties and was
+ # requested, adds it to result.
+ if len(props) > 0:
+ if len(attrs) > 0:
+ if MT_PROPNAME_TO_ATTRNAME[props[0]] in attrs:
+ # if the properties was requested
+ self.log.debug("keep only attribute %s " % (props[0]))
+ result[props[0]] = ent.getValues(attr)
+ else:
+ result[props[0]] = ent.getValues(attr)
+ return result
+
+ def setProperties(self, suffix=None, bename=None, name=None,
+ properties=None):
+ raise NotImplemented()
+
+ def toSuffix(self, entry=None, name=None):
+ '''
+ Return, for a given mapping tree entry, the suffix values.
+ Suffix values are identical from a LDAP point of views.
+ Suffix values may be surrounded by ", or containing '\'
+ escape characters.
+
+ @param entry - LDAP entry of the mapping tree
+ @param name - mapping tree DN
+
+ @result list of values of suffix attribute (aka 'cn')
+
+ @raise ldap.NO_SUCH_OBJECT - in name is invalid DN
+ ValueError - entry does not contains the suffix attribute
+ InvalidArgumentError - if both entry/name are missing
+ '''
+ attr_suffix = MT_PROPNAME_TO_ATTRNAME[MT_SUFFIX]
+ if entry:
+ if not entry.hasValue(attr_suffix):
+ raise ValueError("Entry has no %s attribute %r" %
+ (attr_suffix, entry))
+ return entry.getValues(attr_suffix)
+ elif name:
+ filt = "(objectclass=%s)" % MT_OBJECTCLASS_VALUE
+
+ try:
+ attrs = [attr_suffix]
+ ent = self.conn.getEntry(name, ldap.SCOPE_BASE, filt, attrs)
+ self.log.debug("toSuffix: %s found by its DN" % ent.dn)
+ except NoSuchEntryError:
+ raise ldap.NO_SUCH_OBJECT("mapping tree DN not found: %s" %
+ name)
+
+ if not ent.hasValue(attr_suffix):
+ raise ValueError("Entry has no %s attribute %r" %
+ (attr_suffix, ent))
+ return ent.getValues(attr_suffix)
+ else:
+ raise InvalidArgumentError("entry or name are mandatory")
+
+
+class MappingTree(DSLdapObject):
+ """Mapping tree DSLdapObject with:
+ - must attributes = ['cn']
+ - RDN attribute is 'cn'
+
+ :param instance: A instance
+ :type instance: lib389.DirSrv
+ :param dn: Entry DN
+ :type dn: str
+ :param batch: Not implemented
+ :type batch: bool
+ """
+
+ _must_attributes = ['cn']
+
+ def __init__(self, instance, dn=None, batch=False):
+ super(MappingTree, self).__init__(instance, dn, batch)
+ self._rdn_attribute = 'cn'
+ self._must_attributes = ['cn']
+ self._create_objectclasses = ['top', 'extensibleObject',
MT_OBJECTCLASS_VALUE]
+ self._protected = False
+
+
+class MappingTrees(DSLdapObjects):
+ """DSLdapObjects that presents Mapping trees
+
+ :param instance: A instance
+ :type instance: lib389.DirSrv
+ :param batch: Not implemented
+ :type batch: bool
+ """
+
+ def __init__(self, instance, batch=False):
+ super(MappingTrees, self).__init__(instance=instance, batch=batch)
+ self._objectclasses = [MT_OBJECTCLASS_VALUE]
+ self._filterattrs = ['cn', 'nsslapd-backend' ]
+ self._childobject = MappingTree
+ self._basedn = DN_MAPPING_TREE
+
+
diff --git a/src/lib389/lib389/mit_krb5.py b/src/lib389/lib389/mit_krb5.py
new file mode 100644
index 0000000..64ca24d
--- /dev/null
+++ b/src/lib389/lib389/mit_krb5.py
@@ -0,0 +1,226 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""Mit_krb5 kdc setup routine.
+
+Not ideal for use for a PRD krb system, used internally for testing GSSAPI
+integration with 389ds.
+
+"""
+# In the future we might add support for an ldap-backed krb realm
+from subprocess import Popen, PIPE
+
+import os
+import signal
+import string
+import random
+import subprocess
+
+from lib389._constants import *
+from socket import getfqdn
+from lib389.utils import getdomainname
+from lib389.tools import DirSrvTools
+from lib389.passwd import password_generate
+
+
+class MitKrb5(object):
+ # Get the realm information
+ def __init__(self, realm, warnings=False, debug=False):
+ self.warnings = warnings
+ self.realm = realm.upper()
+ # For the future if we have a non-os krb install.
+ self.krb_prefix = ""
+ sep = os.path.sep
+ self.kadmin = os.path.join(sep, self.krb_prefix,
"usr/sbin/kadmin.local")
+ self.kdb5_util = os.path.join(sep, self.krb_prefix,
"usr/sbin/kdb5_util")
+ self.krb5kdc = os.path.join(sep, self.krb_prefix, "usr/sbin/krb5kdc")
+ self.kdcconf = os.path.join(sep, self.krb_prefix,
"var/kerberos/krb5kdc/kdc.conf")
+ self.kdcpid = os.path.join(sep, self.krb_prefix,
"var/run/krb5kdc.pid")
+ self.krb5conf = os.path.join(sep, self.krb_prefix, "etc/krb5.conf")
+ self.krb5confrealm = os.path.join(sep, self.krb_prefix,
"etc/krb5.conf.d",
+ self.realm.lower().replace('.',
'-'))
+
+ self.krb_master_password = password_generate()
+
+ # Should we write this to a file?
+
+ self.krb_env = {}
+ if debug is True:
+ self.krb_env['KRB5_TRACE'] = '/tmp/krb_lib389.trace'
+
+ # Validate our hostname is in /etc/hosts, and is fqdn
+ def validate_hostname(self):
+ if getdomainname() == "":
+ # Should we return a message?
+ raise AssertionError("Host does not have a domain name.")
+ DirSrvTools.searchHostsFile(getfqdn())
+
+ # Check if a realm exists or not.
+ # Should we check globally? Or just on this local host.
+ def check_realm(self):
+ p = Popen([self.kadmin, '-r', self.realm, '-q',
'list_principals'],
+ env=self.krb_env, stdout=PIPE, stderr=PIPE)
+ returncode = p.wait()
+ if returncode == 0:
+ return True
+ return False
+
+ # Be able to setup the real
+ def create_realm(self, ignore=False):
+ self.validate_hostname()
+ exists = self.check_realm()
+ if exists is True and ignore is False:
+ raise AssertionError("Realm already exists!")
+ elif exists is True and ignore is True:
+ # Realm exists, continue
+ return
+ # Raise a scary warning about eating your krb settings
+ if self.warnings:
+ print("This will alter / erase your krb5 and kdc settings.")
+ raw_input("Ctrl-C to exit, or press ENTER to continue.")
+ print("Kerberos master password: %s" % self.krb_master_password)
+
+ # If we don't have the directories for this, create them.
+ # but if we create them there is no guarantee this will work ...
+ if not os.path.exists(os.path.dirname(self.krb5confrealm)):
+ os.makedirs(os.path.dirname(self.krb5confrealm))
+ # Put the includedir statement into /etc/krb5.conf
+ include = True
+ with open(self.krb5conf, 'r') as kfile:
+ for line in kfile.readlines():
+ if 'includedir %s/' % os.path.dirname(self.krb5confrealm) \
+ in line:
+ include = False
+ if include is True:
+ with open(self.krb5conf, 'a') as kfile:
+ kfile.write('\nincludedir %s/\n' %
+ os.path.dirname(self.krb5confrealm))
+
+ # Write to /etc/krb5.conf.d/example.com
+ with open(self.krb5confrealm, 'w') as cfile:
+ domainname = getdomainname()
+ fqdn = getfqdn()
+ realm = self.realm
+ lrealm = self.realm.lower()
+ cfile.write("""
+[realms]
+{REALM} = {{
+ kdc = {HOST}
+ admin_server = {HOST}
+}}
+
+[domain_realm]
+.{LREALM} = {REALM}
+{LREALM} = {REALM}
+.{DOMAIN} = {REALM}
+{DOMAIN} = {REALM}
+""".format(HOST=fqdn, REALM=realm, LREALM=lrealm, DOMAIN=domainname))
+
+ # Do we need to edit /var/kerberos/krb5kdc/kdc.conf ?
+ with open(self.kdcconf, 'w') as kfile:
+ kfile.write("""
+[kdcdefaults]
+ kdc_ports = 88
+ kdc_tcp_ports = 88
+
+[realms]
+ {REALM} = {{
+ #master_key_type = aes256-cts
+ acl_file = {PREFIX}/var/kerberos/krb5kdc/kadm5.acl
+ dict_file = /usr/share/dict/words
+ admin_keytab = {PREFIX}/var/kerberos/krb5kdc/kadm5.keytab
+ # Just use strong enctypes
+ supported_enctypes = aes256-cts:normal aes128-cts:normal
+ }}
+
+""".format(REALM=self.realm, PREFIX=self.krb_prefix))
+ # Invoke kdb5_util
+ # Can this use -P
+ p = Popen([self.kdb5_util, 'create', '-r', self.realm,
'-s', '-P',
+ self.krb_master_password], env=self.krb_env)
+ assert(p.wait() == 0)
+ # Start the kdc
+ p = Popen([self.krb5kdc, '-P', self.kdcpid, '-r', self.realm],
+ env=self.krb_env)
+ assert(p.wait() == 0)
+ # ???
+ # PROFIT
+ return
+
+ # Destroy the realm
+ def destroy_realm(self):
+ assert(self.check_realm())
+ if self.warnings:
+ print("This will ERASE your kdc settings.")
+ raw_input("Ctrl-C to exit, or press ENTER to continue.")
+ # If the pid exissts, try to kill it.
+ if os.path.isfile(self.kdcpid):
+ with open(self.kdcpid, 'r') as pfile:
+ pid = int(pfile.readline().strip())
+ try:
+ os.kill(pid, signal.SIGTERM)
+ except OSError:
+ pass
+ os.remove(self.kdcpid)
+
+ p = Popen([self.kdb5_util, 'destroy', '-r', self.realm],
+ env=self.krb_env, stdin=PIPE)
+ p.communicate("yes\n")
+ p.wait()
+ assert(p.returncode == 0)
+ # Should we clean up the configurations we made too?
+ # ???
+ # PROFIT
+ return
+
+ def list_principals(self):
+ assert(self.check_realm())
+ p = Popen([self.kadmin, '-r', self.realm, '-q',
'list_principals'],
+ env=self.krb_env)
+ p.wait()
+
+ # Create princs for services
+ def create_principal(self, principal, password=None):
+ assert(self.check_realm())
+ p = None
+ if password:
+ p = Popen([self.kadmin, '-r', self.realm, '-q',
+ 'add_principal -pw \'%s\' %s@%s' % (password,
principal,
+ self.realm)],
+ env=self.krb_env)
+ else:
+ p = Popen([self.kadmin, '-r', self.realm, '-q',
+ 'add_principal -randkey %s@%s' % (principal,
+ self.realm)],
+ env=self.krb_env)
+ assert(p.wait() == 0)
+ self.list_principals()
+
+ # Extract KTs for services
+ def create_keytab(self, principal, keytab):
+ assert(self.check_realm())
+ # Remove the old keytab
+ try:
+ os.remove(keytab)
+ except:
+ pass
+ p = Popen([self.kadmin, '-r', self.realm, '-q', 'ktadd -k %s
%s@%s' %
+ (keytab, principal, self.realm)])
+ assert(p.wait() == 0)
+
+
+class KrbClient(object):
+ def __init__(self, principal, keytab, ccache=None):
+ self.krb_prefix = ""
+ self.kdestroy = "/usr/bin/kdestroy"
+ if ccache is not None:
+ os.environ["KRB5CCNAME"] = ccache
+ # Destroy the previous cache if any.
+ subprocess.call(self.kdestroy)
+ # Gssapi has magic that automatically creates things by env vars
+ os.environ["KRB5_CLIENT_KTNAME"] = keytab
diff --git a/src/lib389/lib389/monitor.py b/src/lib389/lib389/monitor.py
new file mode 100644
index 0000000..5226a67
--- /dev/null
+++ b/src/lib389/lib389/monitor.py
@@ -0,0 +1,94 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""
+Monitor class to display current server performance details
+"""
+
+import ldap
+from ldap import filter as ldap_filter
+from lib389._constants import *
+from lib389._mapped_object import DSLdapObject
+
+
+class Monitor(DSLdapObject):
+ """
+ Allows reading of cn=monitor for server statistics.
+ """
+
+ def __init__(self, instance, dn=None, batch=False):
+ super(Monitor, self).__init__(instance=instance, batch=batch)
+ self._dn = DN_MONITOR
+ self._monitor_keys = [
+ 'instanceection',
+ 'currentinstanceections',
+ 'currentconnections',
+ 'opscompleted',
+ 'opsinitiated',
+ 'threads',
+ 'totalinstanceections',
+ 'version',
+ 'currenttime',
+ 'connection',
+ ]
+
+ def status(self):
+ return self.get_attrs_vals(self._monitor_keys)
+
+class MonitorLDBM(DSLdapObject):
+ def __init__(self, instance, dn=None, batch=False):
+ super(MonitorLDBM, self).__init__(instance=instance, batch=batch)
+ self._dn = DN_MONITOR_LDBM
+ self._backend_keys = [
+ 'dbcachehits',
+ 'dbcachetries',
+ 'dbcachehitratio',
+ 'dbcachepagein',
+ 'dbcachepageout',
+ 'dbcacheroevict',
+ 'dbcacherwevict',
+ ]
+
+ def status(self):
+ return self.get_attrs_vals(self._backend_keys)
+
+class MonitorBackend(DSLdapObject):
+ """
+ This is initialised from Backend in backend.py to get the right basedn.
+ """
+
+ def __init__(self, instance, dn=None, batch=False):
+ super(MonitorBackend, self).__init__(instance=instance, dn=dn, batch=batch)
+ self._backend_keys = [
+ 'readonly',
+ 'entrycachehits',
+ 'entrycachetries',
+ 'entrycachehitratio',
+ 'currententrycachesize',
+ 'maxentrycachesize',
+ 'currententrycachecount',
+ 'maxentrycachecount',
+ 'dncachehits',
+ 'dncachetries',
+ 'dncachehitratio',
+ 'currentdncachesize',
+ 'maxdncachesize',
+ 'currentdncachecount',
+ 'maxdncachecount',
+ 'normalizeddncachetries',
+ 'normalizeddncachehits',
+ 'normalizeddncachemisses',
+ 'normalizeddncachehitratio',
+ 'currentnormalizeddncachesize',
+ 'maxnormalizeddncachesize',
+ 'currentnormalizeddncachecount',
+ ]
+
+ def status(self):
+ return self.get_attrs_vals(self._backend_keys)
+
diff --git a/src/lib389/lib389/ns-slapd.valgrind b/src/lib389/lib389/ns-slapd.valgrind
new file mode 100755
index 0000000..96427fe
--- /dev/null
+++ b/src/lib389/lib389/ns-slapd.valgrind
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+if [ ! "$NETSITE_ROOT" ] ; then
+ NETSITE_ROOT=`pwd | sed -e s@/bin/slapd/server@@g`
+fi
+
+# Assumes you have renamed the original ns-slapd binary
+# to ns-slapd.original
+SLAPD=$0.original
+
+# Make sure /var/tmp exists
+VG_LOGDIR=${VG_LOGDIR:-/var/tmp}
+if [ ! -d $VG_LOGDIR ] ; then
+ mkdir -p $VG_LOGDIR || { echo error: could not mkdir -p $VG_LOGDIR ; exit 1; }
+fi
+
+# Run ns-slapd in valgrind
+outputfile=${VG_LOGDIR}/slapd.vg.$$
+CHECKCMD="valgrind -q --tool=memcheck --leak-check=yes --leak-resolution=high
--num-callers=50 --log-file=$outputfile"
+$CHECKCMD $SLAPD "$@"
+
diff --git a/src/lib389/lib389/nss_ssl.py b/src/lib389/lib389/nss_ssl.py
new file mode 100644
index 0000000..90e78f9
--- /dev/null
+++ b/src/lib389/lib389/nss_ssl.py
@@ -0,0 +1,371 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""Helpers for managing NSS databases in Directory Server
+"""
+
+import os
+import sys
+import random
+import string
+import re
+import socket
+# from nss import nss
+from subprocess import check_call, check_output
+from lib389.passwd import password_generate
+
+from lib389.utils import ensure_str, ensure_bytes
+
+KEYBITS = 4096
+CA_NAME = 'Self-Signed-CA'
+CERT_NAME = 'Server-Cert'
+USER_PREFIX = 'user-'
+PIN_TXT = 'pin.txt'
+PWD_TXT = 'pwdfile.txt'
+ISSUER = 'CN=ca.lib389.example.com,O=testing,L=lib389,ST=Queensland,C=AU'
+SELF_ISSUER = 'CN={HOSTNAME},O=testing,L=lib389,ST=Queensland,C=AU'
+VALID = 2
+
+
+class NssSsl(object):
+ def __init__(self, dirsrv, dbpassword=None):
+ self.dirsrv = dirsrv
+ self.log = self.dirsrv.log
+ if dbpassword is None:
+ self.dbpassword = password_generate()
+ else:
+ self.dbpassword = dbpassword
+
+ @property
+ def _certdb(self):
+ # return "sql:%s" % self.dirsrv.get_cert_dir()
+ return self.dirsrv.get_cert_dir()
+
+ def _generate_noise(self, fpath):
+ noise = password_generate(256)
+ with open(fpath, 'w') as f:
+ f.write(noise)
+
+
+ def reinit(self):
+ """
+ Re-init (create) the nss db.
+ """
+ # 48886: The DB that DS ships with is .... well, broken. Purge it!
+ for f in ('key3.db', 'cert8.db', 'key4.db',
'cert9.db', 'secmod.db', 'pkcs11.txt'):
+ try:
+ # Perhaps we should be backing these up instead ...
+ os.remove("%s/%s" % (self.dirsrv.get_cert_dir(), f ))
+ except:
+ pass
+
+ # In the future we may add the needed option to avoid writing the pin
+ # files.
+ # Write the pin.txt, and the pwdfile.txt
+ if not os.path.exists('%s/%s' % (self.dirsrv.get_cert_dir(), PIN_TXT)):
+ with open('%s/%s' % (self.dirsrv.get_cert_dir(), PIN_TXT),
'w') as f:
+ f.write('Internal (Software) Token:%s' % self.dbpassword)
+ if not os.path.exists('%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT)):
+ with open('%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT),
'w') as f:
+ f.write('%s' % self.dbpassword)
+
+ # Init the db.
+ # 48886; This needs to be sql format ...
+ cmd = ['/usr/bin/certutil', '-N', '-d', self._certdb,
'-f', '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT)]
+ self.dirsrv.log.debug("nss cmd: %s" % cmd)
+ result = check_output(cmd)
+ self.dirsrv.log.debug("nss output: %s" % result)
+ return True
+
+ def _db_exists(self):
+ """
+ Check that a nss db exists at the certpath
+ """
+ key3 = os.path.exists("%s/key3.db" % (self.dirsrv.get_cert_dir()))
+ cert8 = os.path.exists("%s/cert8.db" % (self.dirsrv.get_cert_dir()))
+ key4 = os.path.exists("%s/key4.db" % (self.dirsrv.get_cert_dir()))
+ cert9 = os.path.exists("%s/cert9.db" % (self.dirsrv.get_cert_dir()))
+ secmod = os.path.exists("%s/secmod.db" % (self.dirsrv.get_cert_dir()))
+ pkcs11 = os.path.exists("%s/pkcs11.txt" %
(self.dirsrv.get_cert_dir()))
+
+ if ((key3 and cert8 and secmod) or (key4 and cert9 and pkcs11)):
+ return True
+ return False
+
+ def create_rsa_ca(self):
+ """
+ Create a self signed CA.
+ """
+
+ # Create noise.
+ self._generate_noise('%s/noise.txt' % self.dirsrv.get_cert_dir())
+ # Now run the command. Can we do this with NSS native?
+ cmd = [
+ '/usr/bin/certutil',
+ '-S',
+ '-n',
+ CA_NAME,
+ '-s',
+ ISSUER,
+ '-x',
+ '-g',
+ '%s' % KEYBITS,
+ '-t',
+ 'CT,,',
+ '-v',
+ '%s' % VALID,
+ '-d',
+ self._certdb,
+ '-z',
+ '%s/noise.txt' % self.dirsrv.get_cert_dir(),
+ '-f',
+ '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT),
+ ]
+ result = check_output(cmd)
+ self.dirsrv.log.debug("nss output: %s" % result)
+ # Now extract the CAcert to a well know place.
+ # This allows us to point the cacert dir here and it "just works"
+ cmd = [
+ '/usr/bin/certutil',
+ '-L',
+ '-n',
+ CA_NAME,
+ '-d',
+ self._certdb,
+ '-a',
+ ]
+ certdetails = check_output(cmd)
+ with open('%s/ca.crt' % self.dirsrv.get_cert_dir(), 'w') as f:
+ f.write(ensure_str(certdetails))
+ if os.path.isfile('/usr/sbin/cacertdir_rehash'):
+ check_output(['/usr/sbin/cacertdir_rehash',
self.dirsrv.get_cert_dir()])
+ return True
+
+ def _rsa_cert_list(self):
+ cmd = [
+ '/usr/bin/certutil',
+ '-L',
+ '-d',
+ self._certdb,
+ '-f',
+ '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT),
+ ]
+ result = check_output(cmd)
+
+ # We can skip the first few lines. They are junk
+ # IE ['',
+ # 'Certificate Nickname Trust
Attributes',
+ # '
SSL,S/MIME,JAR/XPI',
+ # '',
+ # 'Self-Signed-CA
CTu,u,u',
+ # '']
+ lines = result.split('\n')[4:-1]
+ # Now make the lines usable
+ cert_values = []
+ for line in lines:
+ data = line.split()
+ cert_values.append((data[0], data[1]))
+ return cert_values
+
+ def _rsa_cert_key_exists(self, cert_tuple):
+ name = cert_tuple[0]
+ cmd = [
+ '/usr/bin/certutil',
+ '-K',
+ '-d',
+ self._certdb,
+ '-f',
+ '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT),
+ ]
+ result = check_output(cmd)
+
+ lines = result.split('\n')[1:-1]
+ key_list = []
+ for line in lines:
+ m = re.match('\<(?P<id>.*)\>
(?P<type>\w+)\s+(?P<hash>\w+).*:(?P<name>.+)', line)
+ if name == m.group('name'):
+ return True
+ return False
+
+ def _rsa_cert_is_catrust(self, cert_tuple):
+ trust_flags = cert_tuple[1]
+ (ssl_flag, mime_flag, jar_flag) = trust_flags.split(',')
+ return 'C' in ssl_flag
+
+ def _rsa_cert_is_user(self, cert_tuple):
+ """
+ Check an RSA cert is user trust
+
+ Sadly we can't check for ext key usage, because NSS makes this really hard.
+ """
+ trust_flags = cert_tuple[1]
+ (ssl_flag, mime_flag, jar_flag) = trust_flags.split(',')
+ return 'u' in ssl_flag
+
+ def _rsa_ca_exists(self):
+ """
+ Detect if a self-signed ca exists
+ """
+ have_ca = False
+ cert_list = self._rsa_cert_list()
+ for cert in cert_list:
+ if self._rsa_cert_key_exists(cert) and self._rsa_cert_is_catrust(cert):
+ have_ca = True
+ return have_ca
+
+ def _rsa_key_and_cert_exists(self):
+ """
+ Check if a valid server key and cert pair exist.
+ """
+ have_cert = False
+ cert_list = self._rsa_cert_list()
+ for cert in cert_list:
+ # This could do a better check for !ca, and server attrs
+ if self._rsa_cert_key_exists(cert) and not self._rsa_cert_is_catrust(cert):
+ have_cert = True
+ return have_cert
+
+ def _rsa_user_exists(self, name):
+ """
+ Check if a valid server key and cert pair exist for a user.
+
+ we use the format, user-<name>
+ """
+ have_user = False
+ cert_list = self._rsa_cert_list()
+ for cert in cert_list:
+ if (cert[0] == '%s%s' % (USER_PREFIX, name)):
+ if self._rsa_cert_key_exists(cert) and self._rsa_cert_is_user(cert):
+ have_user = True
+ return have_user
+
+
+ def create_rsa_key_and_cert(self, alt_names=[]):
+ """
+ Create a key and a cert that is signed by the self signed ca
+
+ This will use the hostname from the DS instance, and takes a list of
+ extra names to take.
+ """
+
+ if len(alt_names) == 0:
+ alt_names.append(socket.gethostname())
+ if self.dirsrv.host not in alt_names:
+ alt_names.append(self.dirsrv.host)
+
+ # Create noise.
+ self._generate_noise('%s/noise.txt' % self.dirsrv.get_cert_dir())
+ cmd = [
+ '/usr/bin/certutil',
+ '-S',
+ '-n',
+ CERT_NAME,
+ '-s',
+ SELF_ISSUER.format(HOSTNAME=self.dirsrv.host),
+ # We MUST issue with SANs else ldap wont verify the name.
+ '-8', ','.join(alt_names),
+ '-c',
+ CA_NAME,
+ '-g',
+ '%s' % KEYBITS,
+ '-t',
+ ',,',
+ '-v',
+ '%s' % VALID,
+ '-d',
+ self._certdb,
+ '-z',
+ '%s/noise.txt' % self.dirsrv.get_cert_dir(),
+ '-f',
+ '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT),
+ ]
+
+ result = check_output(cmd)
+ self.dirsrv.log.debug("nss output: %s" % result)
+ return True
+
+ def create_rsa_user(self, name):
+ """
+ Create a key and cert for a user to authenticate to the directory.
+
+ Name is the uid of the account, and will become the CN of the cert.
+ """
+ cmd = [
+ '/usr/bin/certutil',
+ '-S',
+ '-n',
+ '%s%s' % (USER_PREFIX, name),
+ '-s',
+ SELF_ISSUER.format(HOSTNAME=name),
+ '--keyUsage',
+ 'digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment',
+ '--nsCertType',
+ 'sslClient',
+ '--extKeyUsage',
+ 'clientAuth',
+ '-c',
+ CA_NAME,
+ '-g',
+ '%s' % KEYBITS,
+ '-t',
+ ',,',
+ '-v',
+ '%s' % VALID,
+ '-d',
+ self._certdb,
+ '-z',
+ '%s/noise.txt' % self.dirsrv.get_cert_dir(),
+ '-f',
+ '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT),
+ ]
+
+ result = check_output(cmd)
+ self.dirsrv.log.debug("nss output: %s" % result)
+ # Now extract this into PEM files that we can use.
+ # pk12util -o user-william.p12 -d . -k pwdfile.txt -n user-william -W ''
+ check_call([
+ 'pk12util',
+ '-d', self._certdb,
+ '-o', '%s/%s%s.p12' % (self.dirsrv.get_cert_dir(),
USER_PREFIX, name),
+ '-k', '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT),
+ '-n', '%s%s' % (USER_PREFIX, name),
+ '-W', '""'
+ ])
+ # openssl pkcs12 -in user-william.p12 -passin pass:'' -out file.pem
-nocerts -nodes
+ # Extract the key
+ check_call([
+ 'openssl',
+ 'pkcs12',
+ '-in', '%s/%s%s.p12' % (self.dirsrv.get_cert_dir(),
USER_PREFIX, name),
+ '-passin', 'pass:""',
+ '-out', '%s/%s%s.key' % (self.dirsrv.get_cert_dir(),
USER_PREFIX, name),
+ '-nocerts',
+ '-nodes'
+ ])
+ # Extract the cert
+ check_call([
+ 'openssl',
+ 'pkcs12',
+ '-in', '%s/%s%s.p12' % (self.dirsrv.get_cert_dir(),
USER_PREFIX, name),
+ '-passin', 'pass:""',
+ '-out', '%s/%s%s.crt' % (self.dirsrv.get_cert_dir(),
USER_PREFIX, name),
+ '-nokeys',
+ '-clcerts',
+ '-nodes'
+ ])
+ return True
+
+ def get_rsa_user(self, name):
+ """
+ Return a dict of information for ca, key and cert paths for the user id
+ """
+ ca_path = '%s/ca.crt' % self.dirsrv.get_cert_dir()
+ key_path = '%s/%s%s.key' % (self.dirsrv.get_cert_dir(), USER_PREFIX,
name)
+ crt_path = '%s/%s%s.crt' % (self.dirsrv.get_cert_dir(), USER_PREFIX,
name)
+ return {'ca': ca_path, 'key': key_path, 'crt': crt_path}
+
diff --git a/src/lib389/lib389/passwd.py b/src/lib389/lib389/passwd.py
new file mode 100644
index 0000000..f36d73e
--- /dev/null
+++ b/src/lib389/lib389/passwd.py
@@ -0,0 +1,51 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""
+This file contains helpers to generate password hashes compatible for
+Directory Server.
+"""
+
+import subprocess
+import random
+import string
+import os
+import sys
+
+BESTSCHEME = 'SSHA512'
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+# We need a dict of the schemes I think ....
+PWSCHEMES = [
+ 'SHA1',
+ 'SHA256',
+ 'SHA512',
+ 'SSHA',
+ 'SSHA256',
+ 'SSHA512',
+]
+
+
+# How do we feed our prefix into this?
+def password_hash(pw, scheme=BESTSCHEME, bin_dir='/bin'):
+ # Check that the binary exists
+ assert(scheme in PWSCHEMES)
+ pwdhashbin = os.path.join(bin_dir, 'pwdhash')
+ assert(os.path.isfile(pwdhashbin))
+ h = subprocess.check_output([pwdhashbin, '-s', scheme, pw]).strip()
+ return h.decode('utf-8')
+
+
+def password_generate(length=64):
+ pw = None
+ if MAJOR >= 3:
+ pw = [random.choice(string.ascii_letters) for x in range(length - 1)]
+ else:
+ pw = [random.choice(string.letters) for x in xrange(length - 1)]
+ pw.append('%s' % random.randint(0, 9))
+ return "".join(pw)
diff --git a/src/lib389/lib389/paths.py b/src/lib389/lib389/paths.py
new file mode 100644
index 0000000..cc2f6cd
--- /dev/null
+++ b/src/lib389/lib389/paths.py
@@ -0,0 +1,186 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import sys
+import os
+
+from lib389._constants import DIRSRV_STATE_ONLINE
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+if MAJOR >= 3:
+ import configparser
+else:
+ import ConfigParser as configparser
+
+# Read the paths from default.inf
+
+# Create a lazy eval class for paths. When we first access, we re-read
+# the inf. This way, if we never request a path, we never need to read
+# this file. IE remote installs, we shouldn't need to read this.
+
+# Could this actually become a defaults module, and merge with instance/options?
+# Would it handle the versioning requirements and diff we need?
+
+DEFAULTS_PATH = [
+ '/usr/share/dirsrv/inf/defaults.inf',
+ '/usr/local/share/dirsrv/inf/defaults.inf',
+ '/opt/dirsrv/share/dirsrv/inf/defaults.inf',
+ '/opt/local/share/dirsrv/inf/defaults.inf',
+ '/opt/share/dirsrv/inf/defaults.inf',
+]
+
+MUST = [
+ 'product',
+ 'version',
+ 'user',
+ 'group',
+ 'root_dn',
+ 'prefix',
+ 'bin_dir',
+ 'sbin_dir',
+ 'lib_dir',
+ 'data_dir',
+ 'tmp_dir',
+ 'sysconf_dir',
+ 'config_dir',
+ 'schema_dir',
+ 'cert_dir',
+ 'local_state_dir',
+ 'run_dir',
+ 'lock_dir',
+ 'log_dir',
+ 'inst_dir',
+ 'db_dir',
+ 'backup_dir',
+ 'ldif_dir',
+ 'initconfig_dir',
+]
+
+# will need to add the access, error, audit log later.
+
+# This maps a config to (entry, attr).
+# This can be used online, or while the server is offline and we can parse dse.ldif
+
+CONFIG_MAP = {
+ 'user' : ('cn=config', 'nsslapd-localuser'),
+ 'group' : ('cn=config','nsslapd-localuser'), # Is this
correct?
+ 'schema_dir' : ('cn=config','nsslapd-schemadir'),
+ 'cert_dir' : ('cn=config','nsslapd-certdir'),
+ 'lock_dir' : ('cn=config','nsslapd-lockdir'),
+ 'inst_dir' : ('cn=config','nsslapd-instancedir'),
+ 'db_dir' : ('cn=config,cn=ldbm database,cn=plugins,cn=config',
'nsslapd-directory'),
+ 'backup_dir': ('cn=config','nsslapd-bakdir'),
+ 'ldif_dir': ('cn=config','nsslapd-ldifdir'),
+ 'error_log' : ('cn=config', 'nsslapd-errorlog'),
+ 'access_log' : ('cn=config', 'nsslapd-accesslog'),
+ 'audit_log' : ('cn=config', 'nsslapd-auditlog'),
+}
+
+SECTION = 'slapd'
+
+class Paths(object):
+ def __init__(self, serverid=None, instance=None):
+ """
+ Parses and uses a set of default paths from wellknown locations. The list
+ of keys available is from the MUST attribute in this module.
+
+ To use this module:
+
+ p = Paths()
+ p.bindir
+
+ If the defaults.inf is NOT in a wellknown path, this will throw IOError
+ on the first attribute access. If this does not have a value defaults.inf
+ it will raise KeyError that the defaults.inf is not capable of supporting
+ this tool.
+
+ This is lazy evaluated, so the file is read at the "last minute" then
+ the contents are cached. This means that remote tools that don't need
+ to know about paths, shouldn't need to have a copy of 389-ds-base
+ installed to remotely admin a server.
+ """
+ self._defaults_cached = False
+ self._config = None
+ self._serverid = serverid
+ self._instance = instance
+
+ def _get_defaults_loc(self, search_paths):
+ ## THIS IS HOW WE HANDLE A PREFIX INSTALL
+ prefix = os.getenv('PREFIX')
+ if prefix is not None:
+ spath = os.path.join(prefix, 'share/dirsrv/inf/defaults.inf')
+ if os.path.isfile(spath):
+ return spath
+ else:
+ raise IOError('defaults.inf not found in prefixed location %s' %
spath)
+ for spath in search_paths:
+ if os.path.isfile(spath):
+ return spath
+ raise IOError('defaults.inf not found in any wellknown location!')
+
+ def _read_defaults(self):
+ spath = self._get_defaults_loc(DEFAULTS_PATH)
+ self._config = configparser.SafeConfigParser()
+ self._config.read([spath])
+ self._defaults_cached = True
+
+ def _validate_defaults(self):
+ if self._defaults_cached is False:
+ return False
+ for k in MUST:
+ if self._config.has_option(SECTION, k) is False:
+ raise KeyError('Invalid defaults.inf, missing key %s' % k)
+ return True
+
+ def __getattr__(self, name):
+ from lib389.utils import ensure_str
+ if self._defaults_cached is False:
+ self._read_defaults()
+ self._validate_defaults()
+ # Are we online? Is our key in the config map?
+ if name in CONFIG_MAP and self._instance is not None and self._instance.state ==
DIRSRV_STATE_ONLINE:
+ # Get the online value.
+ (dn, attr) = CONFIG_MAP[name]
+ ent = self._instance.getEntry(dn, attrlist=[attr,])
+ return ensure_str(ent.getValue(attr))
+
+ elif self._serverid is not None:
+ return ensure_str(self._config.get(SECTION,
name).format(instance_name=self._serverid))
+ else:
+ return ensure_str(self._config.get(SECTION, name))
+
+ @property
+ def asan_enabled(self):
+ if self._defaults_cached is False:
+ self._read_defaults()
+ self._validate_defaults()
+ if self._config.has_option(SECTION, 'asan_enabled'):
+ if self._config.get(SECTION, 'asan_enabled') == '1':
+ return True
+ return False
+
+ @property
+ def with_systemd(self):
+ if self._defaults_cached is False:
+ self._read_defaults()
+ self._validate_defaults()
+ if self._config.has_option(SECTION, 'with_systemd'):
+ if self._config.get(SECTION, 'with_systemd') == '1':
+ return True
+ return False
+
+ @property
+ def perl_enabled(self):
+ if self._defaults_cached is False:
+ self._read_defaults()
+ self._validate_defaults()
+ if self._config.has_option(SECTION, 'enable_perl'):
+ if self._config.get(SECTION, 'enable_perl') == 'no':
+ return False
+ return True
diff --git a/src/lib389/lib389/plugins.py b/src/lib389/lib389/plugins.py
new file mode 100644
index 0000000..d683f21
--- /dev/null
+++ b/src/lib389/lib389/plugins.py
@@ -0,0 +1,813 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+import copy
+import os.path
+
+from lib389 import tasks
+from lib389._mapped_object import DSLdapObjects, DSLdapObject
+from lib389.exceptions import Error
+from lib389.lint import DSRILE0001
+from lib389._constants import DN_PLUGIN
+from lib389.properties import (
+ PLUGINS_OBJECTCLASS_VALUE, PLUGIN_PROPNAME_TO_ATTRNAME,
+ PLUGINS_ENABLE_ON_VALUE, PLUGINS_ENABLE_OFF_VALUE, PLUGIN_ENABLE
+ )
+
+class Plugin(DSLdapObject):
+ _plugin_properties = {
+ 'nsslapd-pluginEnabled' : 'off'
+ }
+
+ def __init__(self, instance, dn=None, batch=False):
+ super(Plugin, self).__init__(instance, dn, batch)
+ self._rdn_attribute = 'cn'
+ self._must_attributes = [
+ 'nsslapd-pluginEnabled',
+ 'nsslapd-pluginPath',
+ 'nsslapd-pluginInitfunc',
+ 'nsslapd-pluginType',
+ 'nsslapd-pluginId',
+ 'nsslapd-pluginVendor',
+ 'nsslapd-pluginVersion',
+ 'nsslapd-pluginDescription',
+ ]
+ self._create_objectclasses = ['top', 'nsslapdplugin']
+ # We'll mark this protected, and people can just disable the plugins.
+ self._protected = True
+
+ def enable(self):
+ self.set('nsslapd-pluginEnabled', 'on')
+
+ def disable(self):
+ self.set('nsslapd-pluginEnabled', 'off')
+
+ def status(self):
+ return self.get_attr_val_utf8('nsslapd-pluginEnabled') == 'on'
+
+ def create(self, rdn=None, properties=None, basedn=None):
+ # When we create plugins, we don't want people to have to consider all
+ # the little details. Plus, the server during creation needs to be able
+ # to create these from nothing.
+ # As a result, all the named plugins carry a default properties
+ # dictionary that can be used.
+
+ # Copy the plugin internal properties.
+ internal_properties = copy.deepcopy(self._plugin_properties)
+ if properties is not None:
+ internal_properties.update(properties)
+ return super(Plugin, self).create(rdn, internal_properties, basedn)
+
+class AddnPlugin(Plugin):
+ def __init__(self, instance, dn="cn=addn,cn=plugins,cn=config",
batch=False):
+ super(AddnPlugin, self).__init__(instance, dn, batch)
+ # Need to add wrappers to add domains to this.
+
+class AttributeUniquenessPlugin(Plugin):
+ def __init__(self, instance, dn="cn=attribute
uniqueness,cn=plugins,cn=config", batch=False):
+ super(AttributeUniquenessPlugin, self).__init__(instance, dn, batch)
+
+ ## These are some wrappers to the important attributes
+ # This plugin will be "tricky" in that it can have "many"
instance
+ # of the plugin, rather than many configs.
+
+ def add_unique_attribute(self, attr):
+ self.add('uniqueness-attribute-name', attr)
+
+ def remove_unique_attribute(self, attr):
+ self.remove('uniqueness-attribute-name', attr)
+
+ def add_unique_subtree(self, basedn):
+ self.add('uniqueness-subtrees', basedn)
+
+ def remove_unique_subtree(self, basedn):
+ self.remove('uniqueness-subtrees', basedn)
+
+ def enable_all_subtrees(self):
+ self.set('uniqueness-across-all-subtrees', 'on')
+
+ def disable_all_subtrees(self):
+ self.set('uniqueness-across-all-subtrees', 'off')
+
+class LdapSSOTokenPlugin(Plugin):
+ _plugin_properties = {
+ 'cn' : 'ldapssotoken',
+ 'nsslapd-pluginEnabled' : 'off',
+ 'nsslapd-pluginPath' : 'liblst-plugin',
+ 'nsslapd-pluginInitfunc' : 'lst_init',
+ 'nsslapd-pluginType' : 'extendedop',
+ 'nsslapd-pluginId' : 'ldapssotoken-plugin',
+ 'nsslapd-pluginVendor' : '389 Project',
+ 'nsslapd-pluginVersion' : '1.3.6',
+ 'nsslapd-pluginDescription' : 'Ldap SSO Token Sasl Mech -
draft-wibrown-ldapssotoken',
+ }
+
+ def __init__(self, instance, dn="cn=ldapssotoken,cn=plugins,cn=config",
batch=False):
+ super(LdapSSOTokenPlugin, self).__init__(instance, dn, batch)
+
+class ManagedEntriesPlugin(Plugin):
+ def __init__(self, instance, dn="cn=managed entries,cn=plugins,cn=config",
batch=False):
+ super(ManagedEntriesPlugin, self).__init__(instance, dn, batch)
+
+ # This will likely need to be a bit like both the DSLdapObjects AND the object.
+ # Because there are potentially many MEP configs.
+
+class ReferentialIntegrityPlugin(Plugin):
+ _plugin_properties = {
+ 'cn' : 'referential integrity postoperation',
+ 'nsslapd-pluginEnabled': 'off',
+ 'nsslapd-pluginPath': 'libreferint-plugin',
+ 'nsslapd-pluginInitfunc': 'referint_postop_init',
+ 'nsslapd-pluginType': 'betxnpostoperation',
+ 'nsslapd-pluginprecedence': '40',
+ 'nsslapd-plugin-depends-on-type': 'database',
+ 'referint-update-delay': '0',
+ 'referint-membership-attr': ['member', 'uniquemember',
'owner', 'seeAlso',],
+ 'nsslapd-pluginId' : 'referint',
+ 'nsslapd-pluginVendor' : '389 Project',
+ 'nsslapd-pluginVersion' : '1.3.7.0',
+ 'nsslapd-pluginDescription' : 'referential integrity plugin',
+ }
+
+ def __init__(self, instance, dn="cn=referential integrity
postoperation,cn=plugins,cn=config", batch=False):
+ super(ReferentialIntegrityPlugin, self).__init__(instance, dn, batch)
+ self._create_objectclasses.extend(['extensibleObject'])
+ self._must_attributes.extend([
+ 'referint-update-delay',
+ 'referint-logfile',
+ 'referint-membership-attr',
+ ])
+ self._lint_functions = [self._lint_update_delay]
+
+ def create(self, rdn=None, properties=None, basedn=None):
+ referint_log = os.path.join(self._instance.ds_paths.log_dir,
"referint")
+ if properties is None:
+ properties = {'referint-logfile': referint_log}
+ else:
+ properties['referint-logfile'] = referint_log
+ return super(ReferentialIntegrityPlugin, self).create(rdn, properties, basedn)
+
+ def _lint_update_delay(self):
+ if self.status():
+ delay = self.get_attr_val_int("referint-update-delay")
+ if delay is not None and delay != 0:
+ return DSRILE0001
+
+ def get_update_delay(self):
+ return self.get_attr_val_int('referint-update-delay')
+
+ def get_update_delay_formatted(self):
+ return self.display_attr('referint-update-delay')
+
+ def set_update_delay(self, value):
+ self.set('referint-update-delay', str(value))
+
+ def get_membership_attr(self, formatted=False):
+ return self.get_attr_vals_utf8('referint-membership-attr')
+
+ def get_membership_attr_formatted(self):
+ return self.display_attr('referint-membership-attr')
+
+ def add_membership_attr(self, attr):
+ self.add('referint-membership-attr', attr)
+
+ def remove_membership_attr(self, attr):
+ self.remove('referint-membership-attr', attr)
+
+ def get_entryscope(self, formatted=False):
+ return self.get_attr_vals_utf8('nsslapd-pluginentryscope')
+
+ def get_entryscope_formatted(self):
+ return self.display_attr('nsslapd-pluginentryscope')
+
+ def add_entryscope(self, attr):
+ self.add('nsslapd-pluginentryscope', attr)
+
+ def remove_entryscope(self, attr):
+ self.remove('nsslapd-pluginentryscope', attr)
+
+ def remove_all_entryscope(self):
+ self.remove_all('nsslapd-pluginentryscope')
+
+ def get_excludescope(self):
+ return self.get_attr_vals_ut8('nsslapd-pluginexcludeentryscope')
+
+ def get_excludescope_formatted(self):
+ return self.display_attr('nsslapd-pluginexcludeentryscope')
+
+ def add_excludescope(self, attr):
+ self.add('nsslapd-pluginexcludeentryscope', attr)
+
+ def remove_excludescope(self, attr):
+ self.remove('nsslapd-pluginexcludeentryscope', attr)
+
+ def remove_all_excludescope(self):
+ self.remove_all('nsslapd-pluginexcludeentryscope')
+
+ def get_container_scope(self):
+ return self.get_attr_vals_ut8('nsslapd-plugincontainerscope')
+
+ def get_container_scope_formatted(self):
+ return self.display_attr('nsslapd-plugincontainerscope')
+
+ def add_container_scope(self, attr):
+ self.add('nsslapd-plugincontainerscope', attr)
+
+ def remove_container_scope(self, attr):
+ self.remove('nsslapd-plugincontainerscope', attr)
+
+ def remove_all_container_scope(self):
+ self.remove_all('nsslapd-plugincontainerscope')
+
+class SyntaxValidationPlugin(Plugin):
+ def __init__(self, instance, dn="cn=Syntax Validation
Task,cn=plugins,cn=config", batch=False):
+ super(SyntaxValidationPlugin, self).__init__(instance, dn, batch)
+
+class SchemaReloadPlugin(Plugin):
+ def __init__(self, instance, dn="cn=Schema Reload,cn=plugins,cn=config",
batch=False):
+ super(SchemaReloadPlugin, self).__init__(instance, dn, batch)
+
+class StateChangePlugin(Plugin):
+ def __init__(self, instance, dn="cn=State Change
Plugin,cn=plugins,cn=config", batch=False):
+ super(StateChangePlugin, self).__init__(instance, dn, batch)
+
+class ACLPlugin(Plugin):
+ def __init__(self, instance, dn="cn=ACL Plugin,cn=plugins,cn=config",
batch=False):
+ super(ACLPlugin, self).__init__(instance, dn, batch)
+
+class ACLPreoperationPlugin(Plugin):
+ def __init__(self, instance, dn="cn=ACL preoperation,cn=plugins,cn=config",
batch=False):
+ super(ACLPreoperationPlugin, self).__init__(instance, dn, batch)
+
+class RolesPlugin(Plugin):
+ def __init__(self, instance, dn="cn=Roles Plugin,cn=plugins,cn=config",
batch=False):
+ super(RolesPlugin, self).__init__(instance, dn, batch)
+
+class MemberOfPlugin(Plugin):
+ _plugin_properties = {
+ 'cn' : 'MemberOf Plugin',
+ 'nsslapd-pluginEnabled' : 'off',
+ 'nsslapd-pluginPath' : 'libmemberof-plugin',
+ 'nsslapd-pluginInitfunc' : 'memberof_postop_init',
+ 'nsslapd-pluginType' : 'betxnpostoperation',
+ 'nsslapd-plugin-depends-on-type' : 'database',
+ 'nsslapd-pluginId' : 'memberof',
+ 'nsslapd-pluginVendor' : '389 Project',
+ 'nsslapd-pluginVersion' : '1.3.7.0',
+ 'nsslapd-pluginDescription' : 'memberof plugin',
+ 'memberOfGroupAttr' : 'member',
+ 'memberOfAttr' : 'memberOf',
+ }
+
+ def __init__(self, instance, dn="cn=MemberOf Plugin,cn=plugins,cn=config",
batch=False):
+ super(MemberOfPlugin, self).__init__(instance, dn, batch)
+ self._create_objectclasses.extend(['extensibleObject'])
+ self._must_attributes.extend(['memberOfGroupAttr',
'memberOfAttr'])
+
+ def get_attr(self):
+ return self.get_attr_val('memberofattr')
+
+ def get_attr_formatted(self):
+ return self.display_attr('memberofattr')
+
+ def set_attr(self, attr):
+ self.set('memberofattr', attr)
+
+ def get_groupattr(self):
+ return self.get_attr_vals('memberofgroupattr')
+
+ def get_groupattr_formatted(self):
+ return self.display_attr('memberofgroupattr')
+
+ def add_groupattr(self, attr):
+ self.add('memberofgroupattr', attr)
+
+ def remove_groupattr(self, attr):
+ self.remove('memberofgroupattr', attr)
+
+ def get_allbackends(self):
+ return self.get_attr_val('memberofallbackends')
+
+ def get_allbackends_formatted(self):
+ return self.display_attr('memberofallbackends')
+
+ def enable_allbackends(self):
+ self.set('memberofallbackends', 'on')
+
+ def disable_allbackends(self):
+ self.set('memberofallbackends', 'off')
+
+ def get_skipnested(self):
+ return self.get_attr_val('memberofskipnested')
+
+ def get_skipnested_formatted(self):
+ return self.display_attr('memberofskipnested')
+
+ def enable_skipnested(self):
+ self.set('memberofskipnested', 'on')
+
+ def disable_skipnested(self):
+ self.set('memberofskipnested', 'off')
+
+ def get_autoaddoc(self):
+ return self.get_attr_val('memberofautoaddoc')
+
+ def get_autoaddoc_formatted(self):
+ return self.display_attr('memberofautoaddoc')
+
+ def set_autoaddoc(self, object_class):
+ self.set('memberofautoaddoc', object_class)
+
+ def remove_autoaddoc(self):
+ self.remove_all('memberofautoaddoc')
+
+ def get_entryscope(self, formatted=False):
+ return self.get_attr_vals('memberofentryscope')
+
+ def get_entryscope_formatted(self):
+ return self.display_attr('memberofentryscope')
+
+ def add_entryscope(self, attr):
+ self.add('memberofentryscope', attr)
+
+ def remove_entryscope(self, attr):
+ self.remove('memberofentryscope', attr)
+
+ def remove_all_entryscope(self):
+ self.remove_all('memberofentryscope')
+
+ def get_excludescope(self):
+ return self.get_attr_vals('memberofentryscopeexcludesubtree')
+
+ def get_excludescope_formatted(self):
+ return self.display_attr('memberofentryscopeexcludesubtree')
+
+ def add_excludescope(self, attr):
+ self.add('memberofentryscopeexcludesubtree', attr)
+
+ def remove_excludescope(self, attr):
+ self.remove('memberofentryscopeexcludesubtree', attr)
+
+ def remove_all_excludescope(self):
+ self.remove_all('memberofentryscopeexcludesubtree')
+
+ def fixup(self, basedn, _filter=None):
+ task = tasks.MemberOfFixupTask(self._instance)
+ task_properties = {'basedn': basedn}
+ if _filter is not None:
+ task_properties['filter'] = _filter
+ task.create(properties=task_properties)
+
+ return task
+
+class RetroChangelogPlugin(Plugin):
+ def __init__(self, instance, dn="cn=Retro Changelog
Plugin,cn=plugins,cn=config", batch=False):
+ super(RetroChangelogPlugin, self).__init__(instance, dn, batch)
+
+class ClassOfServicePlugin(Plugin):
+ def __init__(self, instance, dn="cn=Class of Service,cn=plugins,cn=config",
batch=False):
+ super(ClassOfServicePlugin, self).__init__(instance, dn, batch)
+
+class ViewsPlugin(Plugin):
+ def __init__(self, instance, dn="cn=Views,cn=plugins,cn=config",
batch=False):
+ super(ViewsPlugin, self).__init__(instance, dn, batch)
+
+class SevenBitCheckPlugin(Plugin):
+ def __init__(self, instance, dn="cn=7-bit check,cn=plugins,cn=config",
batch=False):
+ super(SevenBitCheckPlugin, self).__init__(instance, dn, batch)
+
+class AccountUsabilityPlugin(Plugin):
+ def __init__(self, instance, dn="cn=Account Usability
Plugin,cn=plugins,cn=config", batch=False):
+ super(AccountUsabilityPlugin, self).__init__(instance, dn, batch)
+
+class AutoMembershipPlugin(Plugin):
+ def __init__(self, instance, dn="cn=Auto Membership
Plugin,cn=plugins,cn=config", batch=False):
+ super(AutoMembershipPlugin, self).__init__(instance, dn, batch)
+
+class ContentSynchronizationPlugin(Plugin):
+ def __init__(self, instance, dn="cn=Content
Synchronization,cn=plugins,cn=config", batch=False):
+ super(ContentSynchronizationPlugin, self).__init__(instance, dn, batch)
+
+class DereferencePlugin(Plugin):
+ def __init__(self, instance, dn="cn=deref,cn=plugins,cn=config",
batch=False):
+ super(DereferencePlugin, self).__init__(instance, dn, batch)
+
+class HTTPClientPlugin(Plugin):
+ def __init__(self, instance, dn="cn=HTTP Client,cn=plugins,cn=config",
batch=False):
+ super(HTTPClientPlugin, self).__init__(instance, dn, batch)
+
+class LinkedAttributesPlugin(Plugin):
+ def __init__(self, instance, dn="cn=Linked
Attributes,cn=plugins,cn=config", batch=False):
+ super(LinkedAttributesPlugin, self).__init__(instance, dn, batch)
+
+class PassThroughAuthenticationPlugin(Plugin):
+ def __init__(self, instance, dn="cn=Pass Through
Authentication,cn=plugins,cn=config", batch=False):
+ super(PassThroughAuthenticationPlugin, self).__init__(instance, dn, batch)
+
+class USNPlugin(Plugin):
+ _plugin_properties = {
+ 'cn' : 'USN',
+ 'nsslapd-pluginEnabled': 'off',
+ 'nsslapd-pluginPath': 'libusn-plugin',
+ 'nsslapd-pluginInitfunc': 'usn_init',
+ 'nsslapd-pluginType': 'object',
+ 'nsslapd-pluginbetxn': 'on',
+ 'nsslapd-plugin-depends-on-type': 'database',
+ 'nsslapd-pluginId': 'USN',
+ 'nsslapd-pluginVendor': '389 Project',
+ 'nsslapd-pluginVersion': '1.3.7.0',
+ 'nsslapd-pluginDescription': 'USN (Update Sequence Number)
plugin',
+ }
+
+ def __init__(self, instance, dn="cn=USN,cn=plugins,cn=config",
batch=False):
+ super(USNPlugin, self).__init__(instance, dn, batch)
+ self._create_objectclasses.extend(['extensibleObject'])
+
+ def is_global_mode_set(self):
+ """Return True if global mode is enabled, else
False."""
+ return self._instance.config.get_attr_val_utf8('nsslapd-entryusn-global')
== 'on'
+
+ def enable_global_mode(self):
+ self._instance.config.set('nsslapd-entryusn-global', 'on')
+
+ def disable_global_mode(self):
+ self._instance.config.set('nsslapd-entryusn-global', 'off')
+
+ def cleanup(self, suffix=None, backend=None, max_usn=None):
+ task = tasks.USNTombstoneCleanupTask(self._instance)
+ task_properties = {}
+
+ if suffix is not None:
+ task_properties['suffix'] = suffix
+ if backend is not None:
+ task_properties['backend'] = backend
+ if max_usn is not None:
+ task_properties['maxusn_to_delete'] = str(max_usn)
+
+ task.create(properties=task_properties)
+
+ return task
+
+class WhoamiPlugin(Plugin):
+ _plugin_properties = {
+ 'cn' : 'whoami',
+ 'nsslapd-pluginEnabled' : 'on',
+ 'nsslapd-pluginPath' : 'libwhoami-plugin',
+ 'nsslapd-pluginInitfunc' : 'whoami_init',
+ 'nsslapd-pluginType' : 'extendedop',
+ 'nsslapd-plugin-depends-on-type' : 'database',
+ 'nsslapd-pluginId' : 'ldapwhoami-plugin',
+ 'nsslapd-pluginVendor' : '389 Project',
+ 'nsslapd-pluginVersion' : '1.3.6',
+ 'nsslapd-pluginDescription' : 'Provides whoami extended
operation',
+ }
+
+ def __init__(self, instance, dn="cn=whoami,cn=plugins,cn=config",
batch=False):
+ super(WhoamiPlugin, self).__init__(instance, dn, batch)
+
+class RootDNAccessControlPlugin(Plugin):
+ _plugin_properties = {
+ 'cn' : 'RootDN Access Control',
+ 'nsslapd-pluginEnabled' : 'off',
+ 'nsslapd-pluginPath' : 'librootdn-access-plugin',
+ 'nsslapd-pluginInitfunc' : 'rootdn_init',
+ 'nsslapd-pluginType' : 'internalpreoperation',
+ 'nsslapd-plugin-depends-on-type' : 'database',
+ 'nsslapd-pluginId' : 'RootDN Access Control',
+ 'nsslapd-pluginVendor' : '389 Project',
+ 'nsslapd-pluginVersion' : '1.3.6',
+ 'nsslapd-pluginDescription' : 'RootDN Access Control plugin',
+ }
+
+ def __init__(self, instance, dn="cn=RootDN Access
Control,cn=plugins,cn=config", batch=False):
+ super(RootDNAccessControlPlugin, self).__init__(instance, dn, batch)
+ self._create_objectclasses.extend(['rootDNPluginConfig'])
+
+ def get_open_time(self):
+ return self.get_attr_val_utf8('rootdn-open-time')
+
+ def get_open_time_formatted(self):
+ return self.display_attr('rootdn-open-time')
+
+ def set_open_time(self, attr):
+ self.set('rootdn-open-time', attr)
+
+ def remove_open_time(self):
+ self.remove_all('rootdn-open-time')
+
+ def get_close_time(self):
+ return self.get_attr_val_utf8('rootdn-close-time')
+
+ def get_close_time_formatted(self):
+ return self.display_attr('rootdn-close-time')
+
+ def set_close_time(self, attr):
+ self.set('rootdn-close-time', attr)
+
+ def remove_close_time(self):
+ self.remove_all('rootdn-close-time')
+
+ def get_days_allowed(self):
+ return self.get_attr_val_utf8('rootdn-days-allowed')
+
+ def get_days_allowed_formatted(self):
+ return self.display_attr('rootdn-days-allowed')
+
+ def set_days_allowed(self, attr):
+ self.set('rootdn-days-allowed', attr)
+
+ def remove_days_allowed(self):
+ self.remove_all('rootdn-days-allowed')
+
+ def add_allow_day(self, day):
+ days = self.get_days_allowed()
+ if days is None:
+ days = ""
+ days = self.add_day_to_days(days, day)
+ if days:
+ self.set_days_allowed(days)
+ else:
+ self.remove_days_allowed()
+
+ def remove_allow_day(self, day):
+ days = self.get_days_allowed()
+ if days is None:
+ days = ""
+ days = self.remove_day_from_days(days, day)
+ if days:
+ self.set_days_allowed(days)
+ else:
+ self.remove_days_allowed()
+
+ def get_allow_host(self):
+ return self.get_attr_val_utf8('rootdn-allow-host')
+
+ def get_allow_host_formatted(self):
+ return self.display_attr('rootdn-allow-host')
+
+ def add_allow_host(self, attr):
+ self.add('rootdn-allow-host', attr)
+
+ def remove_allow_host(self, attr):
+ self.remove('rootdn-allow-host', attr)
+
+ def remove_all_allow_host(self):
+ self.remove_all('rootdn-allow-host')
+
+ def get_deny_host(self):
+ return self.get_attr_val_utf8('rootdn-deny-host')
+
+ def get_deny_host_formatted(self):
+ return self.display_attr('rootdn-deny-host')
+
+ def add_deny_host(self, attr):
+ self.add('rootdn-deny-host', attr)
+
+ def remove_deny_host(self, attr):
+ self.remove('rootdn-deny-host', attr)
+
+ def remove_all_deny_host(self):
+ self.remove_all('rootdn-deny-host')
+
+ def get_allow_ip(self):
+ return self.get_attr_val_utf8('rootdn-allow-ip')
+
+ def get_allow_ip_formatted(self):
+ return self.display_attr('rootdn-allow-ip')
+
+ def add_allow_ip(self, attr):
+ self.add('rootdn-allow-ip', attr)
+
+ def remove_allow_ip(self, attr):
+ self.remove('rootdn-allow-ip', attr)
+
+ def remove_all_allow_ip(self):
+ self.remove_all('rootdn-allow-ip')
+
+ def get_deny_ip(self):
+ return self.get_attr_val_utf8('rootdn-deny-ip')
+
+ def get_deny_ip_formatted(self):
+ return self.display_attr('rootdn-deny-ip')
+
+ def add_deny_ip(self, attr):
+ self.add('rootdn-deny-ip', attr)
+
+ def remove_deny_ip(self, attr):
+ self.remove('rootdn-deny-ip', attr)
+
+ def remove_all_deny_ip(self):
+ self.remove_all('rootdn-deny-ip')
+
+ @staticmethod
+ def add_day_to_days(string_of_days, day):
+ """
+ Append a day in a string of comma seperated days and return the string.
+ If day already exists in the string, return processed string.
+
+ Keyword arguments:
+ string_of_days -- a string of comma seperated days
+ examples:
+ Mon
+ Tue, Wed, Thu
+ day -- a day, e.g. Mon, Tue, etc.
+ """
+ days = [i.strip() for i in string_of_days.split(',') if i]
+
+ if not day in days:
+ days.append(day)
+
+ return ", ".join(days)
+
+ @staticmethod
+ def remove_day_from_days(string_of_days, day):
+ """
+ Remove a day from a string of comma seperated days and return the string.
+ If day does not exists in the string, return processed string.
+
+ Keyword arguments:
+ string_of_days -- a string of comma seperated days
+ examples:
+ Mon
+ Tue, Wed, Thu
+ day -- a day, e.g. Mon, Tue, etc.
+ """
+ days = [i.strip() for i in string_of_days.split(',') if i]
+
+ if day in days:
+ days.remove(day)
+
+ return ", ".join(days)
+
+
+class LDBMBackendPlugin(Plugin):
+ def __init__(self, instance, dn="cn=ldbm database,cn=plugins,cn=config",
batch=False):
+ super(LDBMBackendPlugin, self).__init__(instance, dn, batch)
+
+class ChainingBackendPlugin(Plugin):
+ def __init__(self, instance, dn="cn=chaining
database,cn=plugins,cn=config", batch=False):
+ super(ChainingBackendPlugin, self).__init__(instance, dn, batch)
+
+class AccountPolicyPlugin(Plugin):
+ def __init__(self, instance, dn="cn=Account Policy
Plugin,cn=plugins,cn=config", batch=False):
+ super(AccountPolicyPlugin, self).__init__(instance, dn, batch)
+
+class Plugins(DSLdapObjects):
+
+ # This is a map of plugin to type, so when we
+ # do a get / list / create etc, we can map to the correct
+ # instance.
+ def __init__(self, instance, batch=False):
+ super(Plugins, self).__init__(instance=instance, batch=batch)
+ self._objectclasses = ['top', 'nsslapdplugin']
+ self._filterattrs = ['cn', 'nsslapd-pluginPath']
+ self._childobject = Plugin
+ self._basedn = 'cn=plugins,cn=config'
+ # This is used to allow entry to instance to work
+ self._list_attrlist = ['dn', 'nsslapd-pluginPath']
+ # This may not work for attr unique which can have many instance ....
+ # Should we be doing this from the .so name?
+ self._pluginmap = {
+ 'libaddn-plugin' : AddnPlugin,
+ 'libattr-unique-plugin' : AttributeUniquenessPlugin,
+ 'liblst-plugin' : LdapSSOTokenPlugin,
+ 'libmanagedentries-plugin' : ManagedEntriesPlugin,
+ 'libreferint-plugin' : ReferentialIntegrityPlugin,
+ }
+
+ def _entry_to_instance(self, dn=None, entry=None):
+ # If dn in self._pluginmap
+ if entry['nsslapd-pluginPath'] in self._pluginmap:
+ return self._pluginmap[entry['nsslapd-pluginPath']](self._instance,
dn=dn, batch=self._batch)
+ else:
+ return super(Plugins, self)._entry_to_instance(dn)
+
+
+ # To maintain compat with pluginslegacy, here are some helpers.
+
+ def enable(self, name=None, plugin_dn=None):
+ if plugin_dn is not None:
+ raise ValueError('You should swap to the new Plugin API!')
+ if name is None:
+ raise ldap.NO_SUCH_OBJECT('Must provide a selector for name')
+ plugin = self.get(selector=name)
+ plugin.enable()
+
+ def disable(self, name=None, plugin_dn=None):
+ if plugin_dn is not None:
+ raise ValueError('You should swap to the new Plugin API!')
+ if name is None:
+ raise ldap.NO_SUCH_OBJECT('Must provide a selector for name')
+ plugin = self.get(selector=name)
+ plugin.disable()
+
+
+class PluginsLegacy(object):
+
+ proxied_methods = 'search_s getEntry'.split()
+
+ def __init__(self, conn):
+ """@param conn - a DirSrv instance"""
+ self.conn = conn
+ self.log = conn.log
+
+ def __getattr__(self, name):
+ if name in Plugins.proxied_methods:
+ from lib389 import DirSrv
+ return DirSrv.__getattr__(self.conn, name)
+
+ def list(self, name=None):
+ '''
+ Returns a search result of the plugins entries with all their
+ attributes
+
+ If 'name' is not specified, it returns all the plugins, else it
+ returns the plugin matching ('cn=<name>, DN_PLUGIN')
+
+ @param name - name of the plugin
+
+ @return plugin entries
+
+ @raise None
+
+ '''
+
+ if name:
+ filt = "(objectclass=%s)" % PLUGINS_OBJECTCLASS_VALUE
+ base = "cn=%s,%s" % (name, DN_PLUGIN)
+ scope = ldap.SCOPE_BASE
+ else:
+ filt = "(objectclass=%s)" % PLUGINS_OBJECTCLASS_VALUE
+ base = DN_PLUGIN
+ scope = ldap.SCOPE_ONELEVEL
+
+ ents = self.conn.search_s(base, scope, filt)
+ return ents
+
+ def enable(self, name=None, plugin_dn=None):
+ '''
+ Enable a plugin
+
+ If 'plugin_dn' and 'name' are provided, plugin_dn is used
and
+ 'name' is not considered
+
+ @param name - name of the plugin
+ @param plugin_dn - DN of the plugin
+
+ @return None
+
+ @raise ValueError - if 'name' or 'plugin_dn' lead to unknown
plugin
+ InvalidArgumentError - if 'name' and 'plugin_dn' are
missing
+ '''
+
+ dn = plugin_dn or "cn=%s,%s" % (name, DN_PLUGIN)
+ filt = "(objectclass=%s)" % PLUGINS_OBJECTCLASS_VALUE
+
+ if not dn:
+ from lib389 import InvalidArgumentError
+ raise InvalidArgumentError("'name' and 'plugin_dn' are
missing")
+
+ ents = self.conn.search_s(dn, ldap.SCOPE_BASE, filt)
+ if len(ents) != 1:
+ raise ValueError("%s is unknown")
+
+ self.conn.modify_s(dn, [(ldap.MOD_REPLACE,
+ PLUGIN_PROPNAME_TO_ATTRNAME[PLUGIN_ENABLE],
+ PLUGINS_ENABLE_ON_VALUE)])
+
+ def disable(self, name=None, plugin_dn=None):
+ '''
+ Disable a plugin
+
+ If 'plugin_dn' and 'name' are provided, plugin_dn is used
and
+ 'name' is not considered
+
+ @param name - name of the plugin
+ @param plugin_dn - DN of the plugin
+
+ @return None
+
+ @raise ValueError - if 'name' or 'plugin_dn' lead to unknown
plugin
+ InvalidArgumentError - if 'name' and 'plugin_dn' are
missing
+ '''
+
+ dn = plugin_dn or "cn=%s,%s" % (name, DN_PLUGIN)
+ filt = "(objectclass=%s)" % PLUGINS_OBJECTCLASS_VALUE
+
+ if not dn:
+ from lib389 import InvalidArgumentError
+ raise InvalidArgumentError("'name' and 'plugin_dn' are
missing")
+
+ ents = self.conn.search_s(dn, ldap.SCOPE_BASE, filt)
+ if len(ents) != 1:
+ raise ValueError("%s is unknown")
+
+ self.conn.modify_s(dn, [(ldap.MOD_REPLACE,
+ PLUGIN_PROPNAME_TO_ATTRNAME[PLUGIN_ENABLE],
+ PLUGINS_ENABLE_OFF_VALUE)])
diff --git a/src/lib389/lib389/properties.py b/src/lib389/lib389/properties.py
new file mode 100644
index 0000000..2a9e174
--- /dev/null
+++ b/src/lib389/lib389/properties.py
@@ -0,0 +1,498 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+####################################
+#
+# Properties supported by the server
+#
+####################################
+
+from lib389._constants import *
+
+#
+# Those WITH related attribute name
+#
+SER_HOST = 'hostname'
+SER_PORT = 'ldap-port'
+SER_LDAP_URL = 'ldapurl'
+SER_SECURE_PORT = 'ldap-secureport'
+SER_ROOT_DN = 'root-dn'
+SER_ROOT_PW = 'root-pw'
+SER_USER_ID = 'user-id'
+SER_CREATION_SUFFIX = 'suffix'
+SER_LDAPI_ENABLED = 'ldapi_enabled'
+SER_LDAPI_SOCKET = 'ldapi_socket'
+SER_LDAPI_AUTOBIND = 'ldapi_autobind'
+SER_INST_SCRIPTS_ENABLED = 'InstScriptsEnabled'
+
+SER_PROPNAME_TO_ATTRNAME = {SER_HOST: 'nsslapd-localhost',
+ SER_PORT: 'nsslapd-port',
+ SER_SECURE_PORT: 'nsslapd-securePort',
+ SER_ROOT_DN: 'nsslapd-rootdn',
+ # SER_ROOT_PW: 'nsslapd-rootpw', # We can't use
this value ...
+ SER_USER_ID: 'nsslapd-localuser',
+ SER_CREATION_SUFFIX: 'nsslapd-defaultnamingcontext',
+ SER_LDAPI_ENABLED: 'nsslapd-ldapilisten',
+ SER_LDAPI_SOCKET: 'nsslapd-ldapifilepath',
+ SER_LDAPI_AUTOBIND: 'nsslapd-ldapiautobind',
+ }
+#
+# Those WITHOUT related attribute name
+#
+SER_REALM = 'krb5_realm'
+SER_SERVERID_PROP = 'server-id'
+SER_GROUP_ID = 'group-id'
+SER_DEPLOYED_DIR = 'deployed-dir'
+SER_BACKUP_INST_DIR = 'inst-backupdir'
+SER_STRICT_HOSTNAME_CHECKING = 'strict_hostname_checking'
+
+####################################
+#
+# Properties supported by the Mapping Tree entries
+#
+####################################
+
+# Properties name
+MT_STATE = 'state'
+MT_BACKEND = 'backend-name'
+MT_SUFFIX = 'suffix'
+MT_REFERRAL = 'referral'
+MT_CHAIN_PATH = 'chain-plugin-path'
+MT_CHAIN_FCT = 'chain-plugin-fct'
+MT_CHAIN_UPDATE = 'chain-update-policy'
+MT_PARENT_SUFFIX = 'parent-suffix'
+
+# Properties values
+MT_STATE_VAL_DISABLED = 'disabled'
+MT_STATE_VAL_BACKEND = 'backend'
+MT_STATE_VAL_REFERRAL = 'referral'
+MT_STATE_VAL_REFERRAL_ON_UPDATE = 'referral-on-update'
+MT_STATE_VAL_CONTAINER = 'container'
+MT_STATE_TO_VALUES = {MT_STATE_VAL_DISABLED: 0,
+ MT_STATE_VAL_BACKEND: 1,
+ MT_STATE_VAL_REFERRAL: 2,
+ MT_STATE_VAL_REFERRAL_ON_UPDATE: 3,
+ MT_STATE_VAL_CONTAINER: 4}
+
+MT_CHAIN_UPDATE_VAL_ON_UPDATE = 'repl_chain_on_update'
+MT_OBJECTCLASS_VALUE = 'nsMappingTree'
+
+MT_PROPNAME_TO_VALUES = {MT_STATE: MT_STATE_TO_VALUES}
+MT_PROPNAME_TO_ATTRNAME = {MT_STATE: 'nsslapd-state',
+ MT_BACKEND: 'nsslapd-backend',
+ MT_SUFFIX: 'cn',
+ MT_PARENT_SUFFIX: 'nsslapd-parent-suffix',
+ MT_REFERRAL: 'nsslapd-referral',
+ MT_CHAIN_PATH: 'nsslapd-distribution-plugin',
+ MT_CHAIN_FCT: 'nsslapd-distribution-funct',
+ MT_CHAIN_UPDATE: 'nsslapd-distribution-root-update'}
+
+####################################
+#
+# Properties supported by the backend
+#
+####################################
+BACKEND_SUFFIX = 'suffix'
+BACKEND_NAME = 'name'
+BACKEND_READONLY = 'read-only'
+BACKEND_REQ_INDEX = 'require-index'
+BACKEND_CACHE_ENTRIES = 'entry-cache-number'
+BACKEND_CACHE_SIZE = 'entry-cache-size'
+BACKEND_DNCACHE_SIZE = 'dn-cache-size'
+BACKEND_DIRECTORY = 'directory'
+BACKEND_DB_DEADLOCK = 'db-deadlock'
+BACKEND_CHAIN_BIND_DN = 'chain-bind-dn'
+BACKEND_CHAIN_BIND_PW = 'chain-bind-pw'
+BACKEND_CHAIN_URLS = 'chain-urls'
+BACKEND_STATS = 'stats'
+BACKEND_SUFFIX = 'suffix'
+BACKEND_SAMPLE_ENTRIES = 'sample_entries'
+
+BACKEND_OBJECTCLASS_VALUE = 'nsBackendInstance'
+
+BACKEND_PROPNAME_TO_ATTRNAME = {BACKEND_SUFFIX: 'nsslapd-suffix',
+ BACKEND_NAME: 'cn',
+ BACKEND_READONLY: 'nsslapd-readonly',
+ BACKEND_REQ_INDEX: 'nsslapd-require-index',
+ BACKEND_CACHE_ENTRIES: 'nsslapd-cachesize',
+ BACKEND_CACHE_SIZE: 'nsslapd-cachememsize',
+ BACKEND_DNCACHE_SIZE: 'nsslapd-dncachememsize',
+ BACKEND_DIRECTORY: 'nsslapd-directory',
+ BACKEND_DB_DEADLOCK:
+ 'nsslapd-db-deadlock-policy',
+ BACKEND_CHAIN_BIND_DN: 'nsmultiplexorbinddn',
+ BACKEND_CHAIN_BIND_PW:
+ 'nsmultiplexorcredentials',
+ BACKEND_CHAIN_URLS: 'nsfarmserverurl'}
+
+####################################
+#
+# Properties of a replica
+#
+####################################
+
+
+#
+# General attributes (for both replica and agmt)
+#
+REPL_RUV = 'nsds50ruv'
+REPL_ROOT = 'nsDS5ReplicaRoot'
+REPL_PROTOCOL_TIMEOUT = 'nsds5ReplicaProtocolTimeout'
+REPL_BINDDN = 'nsDS5ReplicaBindDN'
+REPL_REAP_ACTIVE = 'nsds5ReplicaReapActive'
+
+#
+# Replica Entry Attributes
+#
+REPL_PURGE_DELAY = 'nsds5ReplicaPurgeDelay'
+REPL_RETRY_MIN = 'nsds5ReplicaBackoffMin'
+REPL_RETRY_MAX = 'nsds5ReplicaBackoffMax'
+REPL_TYPE = 'nsDS5ReplicaType'
+REPL_FLAGS = 'nsDS5Flags'
+REPL_ID = 'nsDS5ReplicaId'
+REPL_STATE = 'nsState'
+REPL_NAME = 'nsDS5ReplicaName'
+REPL_BIND_GROUP = 'nsds5replicabinddngroup'
+REPL_BIND_GROUP_INTERVAL = 'nsds5replicabinddngroupcheckinterval'
+REPL_REF = 'nsds5ReplicaReferral'
+REPL_TOMBSTONE_PURGE_INTERVAL = 'nsds5ReplicaTombstonePurgeInterval'
+REPL_CLEAN_RUV = 'nsds5ReplicaCleanRUV'
+REPL_ABORT_RUV = 'nsds5ReplicaAbortCleanRUV'
+REPL_COUNT_COUNT = 'nsds5ReplicaChangeCount'
+REPL_PRECISE_PURGE = 'nsds5ReplicaPreciseTombstonePurging'
+REPL_RELEASE_TIMEOUT = 'nsds5replicaReleaseTimeout'
+
+# The values are from the REST API
+REPLICA_SUFFIX = 'suffix'
+REPLICA_PURGE_DELAY = 'ReplicaPurgeDelay'
+REPLICA_ROOT = 'ReplicaRoot'
+REPLICA_PROTOCOL_TIMEOUT = 'ReplicaProtocolTimeout'
+REPLICA_BINDDN = 'ReplicaBindDN'
+REPLICA_REAP_ACTIVE = 'ReplicaReapActive'
+REPLICA_RETRY_MIN = 'ReplicaBackoffMin'
+REPLICA_RETRY_MAX = 'ReplicaBackoffMax'
+REPLICA_TYPE = 'ReplicaType'
+REPLICA_FLAGS = 'ReplicaFlags'
+REPLICA_ID = 'Replicaid'
+REPLICA_STATE = 'ReplicaState'
+REPLICA_NAME = 'ReplicaName'
+REPLICA_BIND_GROUP = 'ReplicaBindDNGroup'
+REPLICA_BIND_GROUP_INTERVAL = 'ReplicaBindDNGroupCheckInterval'
+REPLICA_REFERRAL = 'ReplicaReferral'
+REPLICA_TOMBSTONE_PURGE_INTERVAL = 'ReplicaTombstonePurgeInterval'
+REPLICA_PURGE_INTERVAL = REPLICA_TOMBSTONE_PURGE_INTERVAL
+REPLICA_CLEAN_RUV = 'ReplicaCleanRUV'
+REPLICA_ABORT_RUV = 'ReplicaAbortCleanRUV'
+REPLICA_COUNT_COUNT = 'ReplicaChangeCount'
+REPLICA_PRECISE_PURGING = 'ReplicaPreciseTombstonePurging'
+REPLICA_RELEASE_TIMEOUT = 'ReplicaReleaseTimeout'
+
+REPLICA_PROPNAME_TO_ATTRNAME = {REPLICA_SUFFIX: REPL_ROOT,
+ REPLICA_ROOT: REPL_ROOT,
+ REPLICA_ID: REPL_ID,
+ REPLICA_TYPE: REPL_TYPE,
+ REPLICA_PURGE_INTERVAL:
+ REPL_TOMBSTONE_PURGE_INTERVAL,
+ REPLICA_PURGE_DELAY: REPL_PURGE_DELAY,
+ REPLICA_REFERRAL: REPL_REF,
+ REPLICA_PROTOCOL_TIMEOUT:
+ REPL_PROTOCOL_TIMEOUT,
+ REPLICA_BINDDN: REPL_BINDDN,
+ REPLICA_REAP_ACTIVE: REPL_REAP_ACTIVE,
+ REPLICA_RETRY_MIN: REPL_RETRY_MIN,
+ REPLICA_RETRY_MAX: REPL_RETRY_MAX,
+ REPLICA_FLAGS: REPL_FLAGS,
+ REPLICA_STATE: REPL_STATE,
+ REPLICA_NAME: REPL_NAME,
+ REPLICA_BIND_GROUP: REPL_BIND_GROUP,
+ REPLICA_BIND_GROUP_INTERVAL:
+ REPL_BIND_GROUP_INTERVAL,
+ REPLICA_TOMBSTONE_PURGE_INTERVAL:
+ REPL_TOMBSTONE_PURGE_INTERVAL,
+ REPLICA_CLEAN_RUV: REPL_CLEAN_RUV,
+ REPLICA_ABORT_RUV: REPL_ABORT_RUV,
+ REPLICA_COUNT_COUNT: REPL_COUNT_COUNT,
+ REPLICA_PRECISE_PURGING: REPL_PRECISE_PURGE,
+ REPLICA_RELEASE_TIMEOUT: REPL_RELEASE_TIMEOUT}
+
+####################################
+#
+# Properties of a changelog
+#
+####################################
+
+
+#
+# Changelog Attributes
+#
+CL_DIR = 'nsslapd-changelogdir'
+CL_MAX_ENTRIES = 'nsslapd-changelogmaxentries'
+CL_MAXAGE = 'nsslapd-changelogmaxage'
+CL_COMPACT_INTERVAL = 'nsslapd-changelogcompactdb-interval'
+CL_TRIM_INTERVAL = 'nsslapd-changelogtrim-interval'
+CL_CONCURRENT_WRITES = 'nsslapd-changelogmaxconcurrentwrites'
+CL_ENCRYPT_ALG = 'nsslapd-encryptionalgorithm'
+CL_SYM_KEY = 'nsSymmetricKey'
+
+# REST names
+CHANGELOG_NAME = 'cl-name'
+CHANGELOG_DIR = 'ChangelogDir'
+CHANGELOG_MAXAGE = 'ChangelogMaxAge'
+CHANGELOG_MAXENTRIES = 'ChangelogMaxEntries'
+CHANGELOG_TRIM_INTERVAL = 'ChangelogTrimInterval'
+CHANGELOG_COMPACT_INTV = 'ChangelogCompactDBInterval'
+CHANGELOG_CONCURRENT_WRITES = 'ChangelogMaxConcurrentWrites'
+CHANGELOG_ENCRYPT_ALG = 'ChangelogEncryptionAlgorithm'
+CHANGELOG_SYM_KEY = 'ChangelogSymmetricKey'
+
+CL_DIR = 'nsslapd-changelogdir'
+CL_MAX_ENTRIES = 'nsslapd-changelogmaxentries'
+CL_MAXAGE = 'nsslapd-changelogmaxage'
+CL_COMPACT_INTERVAL = 'nsslapd-changelogcompactdb-interval'
+CL_TRIM_INTERVAL = 'nsslapd-changelogtrim-interval'
+CL_CONCURRENT_WRITES = 'nsslapd-changelogmaxconcurrentwrites'
+CL_ENCRYPT_ALG = 'nsslapd-encryptionalgorithm'
+CL_SYM_KEY = 'nsSymmetricKey'
+
+CHANGELOG_PROPNAME_TO_ATTRNAME = {CHANGELOG_NAME: 'cn',
+ CHANGELOG_DIR: CL_DIR,
+ CHANGELOG_MAXAGE: CL_MAXAGE,
+ CHANGELOG_MAXENTRIES: CL_MAX_ENTRIES,
+ CHANGELOG_TRIM_INTERVAL: CL_TRIM_INTERVAL,
+ CHANGELOG_COMPACT_INTV: CL_COMPACT_INTERVAL,
+ CHANGELOG_CONCURRENT_WRITES:
+ CL_CONCURRENT_WRITES,
+ CHANGELOG_ENCRYPT_ALG: CL_ENCRYPT_ALG,
+ CHANGELOG_SYM_KEY: CL_SYM_KEY}
+
+####################################
+#
+# Properties of an entry
+#
+####################################
+ENTRY_OBJECTCLASS = 'objectclass'
+ENTRY_SN = 'sn'
+ENTRY_CN = 'cn'
+ENTRY_TYPE_PERSON = 'person'
+ENTRY_TYPE_INETPERSON = 'inetperson'
+ENTRY_TYPE_GROUP = 'group'
+ENTRY_USERPASSWORD = 'userpassword'
+ENTRY_UID = 'uid'
+
+ENTRY_TYPE_TO_OBJECTCLASS = {ENTRY_TYPE_PERSON: ["top",
+ "person"],
+ ENTRY_TYPE_INETPERSON: ["top",
+ "person",
+ "inetOrgPerson"]}
+
+####################################
+#
+# Properties supported by the replica agreement
+#
+####################################
+
+#
+# Repl Agreement Attributes
+#
+AGMT_BIND_METHOD = 'nsDS5ReplicaBindMethod'
+AGMT_HOST = 'nsDS5ReplicaHost'
+AGMT_PORT = 'nsDS5ReplicaPort'
+AGMT_TRANSPORT_INFO = 'nsDS5ReplicaTransportInfo'
+AGMT_ATTR_LIST = 'nsDS5ReplicatedAttributeList'
+AGMT_ATTR_LIST_TOTAL = 'nsDS5ReplicatedAttributeListTotal'
+AGMT_TIMEOUT = 'nsds5replicaTimeout'
+AGMT_CRED = 'nsDS5ReplicaCredentials'
+AGMT_REFRESH = 'nsds5BeginReplicaRefresh'
+AGMT_SCHEDULE = 'nsds5ReplicaUpdateSchedule'
+AGMT_BUSY_WAIT_TIME = 'nsds5ReplicaBusyWaitTime'
+AGMT_SESSION_PAUSE_TIME = 'nsds5ReplicaSessionPauseTime'
+AGMT_ENABLED = 'nsds5ReplicaEnabled'
+AGMT_STRIP_ATTRS = 'nsds5ReplicaStripAttrs'
+AGMT_FLOW_WINDOW = 'nsds5ReplicaFlowControlWindow'
+AGMT_FLOW_PAUSE = 'nsds5ReplicaFlowControlPause'
+AGMT_MAXCSN = 'nsds5AgmtMaxCSN'
+AGMT_UPDATE_START = 'nsds5replicaLastUpdateStart'
+AGMT_UPDATE_END = 'nsds5replicaLastUpdateEnd'
+AGMT_CHANGES_SINCE_STARTUP = 'nsds5replicaChangesSentSinceStartup' # base64
+AGMT_UPDATE_STATUS = 'nsds5replicaLastUpdateStatus'
+AGMT_UPDATE_IN_PROGRESS = 'nsds5replicaUpdateInProgress'
+AGMT_INIT_START = 'nsds5replicaLastInitStart'
+AGMT_INIT_END = 'nsds5replicaLastInitEnd'
+AGMT_INIT_STATUS = 'nsds5replicaLastInitStatus'
+
+#
+# WinSync Agreement Attributes
+#
+AGMT_WINSYNC_SUBTREE = 'nsds7WindowsReplicaSubtree'
+AGMT_WINSYNC_REPLICA_SUBTREE = 'nsds7DirectoryReplicaSubtree'
+AGMT_WINSYNC_NEW_USERSYNC = 'nsds7NewWinUserSyncEnabled'
+AGMT_WINSYNC_NEW_GROUP_SYNC = 'nsds7NewWinGroupSyncEnabled'
+AGMT_WINSYNC_DOMAIN = 'nsds7WindowsDomain'
+AGMT_WINSYNC_COOKIE = 'nsds7DirsyncCookie'
+AGMT_WINSYNC_INTERVAL = 'winSyncInterval'
+AGMT_WINSYNC_ONE_WAY_SYNC = 'oneWaySync'
+AGMT_WINSYNC_MOVE_ACTION = 'winSyncMoveAction'
+AGMT_WINSYNC_WIN_FILTER = 'winSyncWindowsFilter'
+AGMT_WINSYNC_DIR_FILTER = 'winSyncDirectoryFilter'
+AGMT_WINSYNC_SUBTREE_PAIR = 'winSyncSubtreePair'
+
+RA_NAME = 'ReplicaName'
+RA_SUFFIX = 'ReplicaRoot'
+RA_BINDDN = 'ReplicaBindDN'
+RA_BINDPW = 'ReplicaBindCredentials'
+RA_METHOD = 'ReplicaBindMethod'
+RA_DESCRIPTION = 'description'
+RA_SCHEDULE = 'ReplicaSchedule'
+RA_TRANSPORT_PROT = 'ReplicaTransportInfo'
+RA_FRAC_EXCLUDE = 'ReplicatedAttributeList'
+RA_FRAC_EXCLUDE_TOTAL_UPDATE = 'ReplicatedAttributeListTotal'
+RA_FRAC_STRIP = 'ReplicaStripAttrs'
+RA_CONSUMER_PORT = 'ReplicaPort'
+RA_CONSUMER_HOST = 'ReplicaHost'
+RA_CONSUMER_TOTAL_INIT = 'ReplicaBeginRefresh'
+RA_TIMEOUT = 'ReplicaConnTimeout'
+RA_CHANGES = 'ReplicaChangesSentSinceStartup'
+RA_BUSY_WAIT = 'ReplicaBusyWaitTime'
+RA_PAUSE_TIME = 'ReplicaSessionPauseTime'
+RA_ENABLED = 'ReplicaEnabled'
+RA_FLOW_WINDOW = 'ReplicaFlowControlWindow'
+RA_FLOW_PAUSE = 'ReplicaFlowControlPause'
+RA_RUV = 'ReplicaRUV'
+RA_MAXCSN = 'ReplicaMaxCSN'
+RA_LAST_UPDATE_START = 'ReplicaLastUpdateStart'
+RA_LAST_UPDATE_END = 'ReplicaLastUpdateEnd'
+RA_LAST_UPDATE_STATUS = 'ReplicaLastUpdateStatus'
+RA_UPDATE_IN_PROGRESS = 'ReplicaUpdateInProgress'
+RA_LAST_INIT_START = 'ReplicaLastInitStart'
+RA_LAST_INIT_END = 'ReplicaLastInitEnd'
+RA_LAST_INIT_STATUS = 'ReplicaLastInitStatus'
+
+REPLICA_OBJECTCLASS_VALUE = 'nsds5Replica'
+RA_OBJECTCLASS_VALUE = "nsds5replicationagreement"
+RA_WINDOWS_OBJECTCLASS_VALUE = "nsDSWindowsReplicationAgreement"
+
+RA_PROPNAME_TO_ATTRNAME = {RA_NAME: 'cn',
+ RA_SUFFIX: REPL_ROOT,
+ RA_BINDDN: REPL_BINDDN,
+ RA_BINDPW: AGMT_CRED,
+ RA_METHOD: AGMT_BIND_METHOD,
+ RA_DESCRIPTION: 'description',
+ RA_SCHEDULE: AGMT_SCHEDULE,
+ RA_TRANSPORT_PROT: AGMT_TRANSPORT_INFO,
+ RA_FRAC_EXCLUDE: AGMT_ATTR_LIST,
+ RA_FRAC_EXCLUDE_TOTAL_UPDATE: AGMT_ATTR_LIST_TOTAL,
+ RA_FRAC_STRIP: AGMT_STRIP_ATTRS,
+ RA_CONSUMER_PORT: AGMT_PORT,
+ RA_CONSUMER_HOST: AGMT_HOST,
+ RA_CONSUMER_TOTAL_INIT: AGMT_REFRESH,
+ RA_TIMEOUT: AGMT_TIMEOUT,
+ RA_CHANGES: AGMT_CHANGES_SINCE_STARTUP,
+ RA_BUSY_WAIT: AGMT_BUSY_WAIT_TIME,
+ RA_PAUSE_TIME: AGMT_SESSION_PAUSE_TIME,
+ RA_ENABLED: AGMT_ENABLED,
+ RA_FLOW_WINDOW: AGMT_FLOW_WINDOW,
+ RA_FLOW_PAUSE: AGMT_FLOW_PAUSE,
+ RA_RUV: REPL_RUV,
+ RA_MAXCSN: AGMT_MAXCSN,
+ RA_LAST_UPDATE_START: AGMT_UPDATE_START,
+ RA_LAST_UPDATE_END: AGMT_UPDATE_END,
+ RA_LAST_UPDATE_STATUS: AGMT_UPDATE_STATUS,
+ RA_UPDATE_IN_PROGRESS: AGMT_UPDATE_IN_PROGRESS,
+ RA_LAST_INIT_START: AGMT_INIT_START,
+ RA_LAST_INIT_END: AGMT_INIT_END,
+ RA_LAST_INIT_STATUS: AGMT_INIT_STATUS,
+ REPLICA_REAP_ACTIVE: REPL_REAP_ACTIVE}
+
+####################################
+#
+# Properties supported by the plugins
+#
+####################################
+
+PLUGIN_NAME = "name"
+PLUGIN_PATH = "path"
+PLUGIN_ENABLE = 'enable'
+
+PLUGINS_OBJECTCLASS_VALUE = "nsSlapdPlugin"
+PLUGINS_ENABLE_ON_VALUE = "on"
+PLUGINS_ENABLE_OFF_VALUE = "off"
+
+PLUGIN_PROPNAME_TO_ATTRNAME = {PLUGIN_NAME: 'cn',
+ PLUGIN_PATH: 'nsslapd-pluginPath',
+ PLUGIN_ENABLE: 'nsslapd-pluginEnabled'}
+
+####################################
+#
+# Properties supported by the index
+#
+####################################
+INDEX_TYPE = "type"
+INDEX_SYSTEM = "system"
+INDEX_MATCHING_RULE = "matching-rule"
+
+INDEX_PROPNAME_TO_ATTRNAME = {INDEX_TYPE: 'nsIndexType',
+ INDEX_SYSTEM: 'nsSystemIndex',
+ INDEX_MATCHING_RULE: 'nsMatchingRule'}
+
+####################################
+#
+# Properties supported by the tasks
+#
+####################################
+
+TASK_WAIT = "wait"
+TASK_TOMB_STRIP = "strip-csn"
+EXPORT_REPL_INFO = "repl-info"
+
+
+####################################
+#
+# Properties related to logging.
+#
+####################################
+
+LOG_ACCESS_ENABLED = "nsslapd-accesslog-logging-enabled"
+LOG_ACCESS_LEVEL = "nsslapd-accesslog-level"
+LOG_ACCESS_PATH = "nsslapd-accesslog"
+
+LOG_ERROR_ENABLED = "nsslapd-errorlog-logging-enabled"
+LOG_ERROR_LEVEL = "nsslapd-errorlog-level"
+LOG_ERROR_PATH = "nsslapd-errorlog"
+
+
+def rawProperty(prop):
+ '''
+ Return the string 'prop' without the heading '+'/'-'
+ @param prop - string of a property name
+
+ @return property name without heading '+'/'-'
+
+ @raise None
+ '''
+ if str(prop).startswith('+') or str(prop).startswith('-'):
+ return prop[1::]
+ else:
+ return prop
+
+
+def inProperties(prop, properties):
+ '''
+ Return True if 'prop' is in the 'properties' dictionary
+ Properties in 'properties' does NOT contain a heading
'+'/'-',
+ but 'prop' may contain heading '+'/'-'
+ @param prop - string of a property name
+ @param properties - dictionary of properties.
+
+ @return True/False
+
+ @raise None
+ '''
+ if rawProperty(prop) in properties:
+ return True
+ else:
+ return False
diff --git a/src/lib389/lib389/referral.py b/src/lib389/lib389/referral.py
new file mode 100644
index 0000000..e4500cb
--- /dev/null
+++ b/src/lib389/lib389/referral.py
@@ -0,0 +1,35 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+
+from lib389._mapped_object import DSLdapObjects, DSLdapObject
+
+
+class Referral(DSLdapObject):
+ def __init__(self, instance, dn=None, batch=False):
+ super(Referral, self).__init__(instance, dn, batch)
+ self._rdn_attribute = "cn"
+ self._must_attributes = ["ref"]
+ self._create_objectclasses = ['referral', 'nsContainer']
+ self._protected = False
+ managedsait_ctrl = ldap.controls.simple.ManageDSAITControl()
+ self._server_controls = [managedsait_ctrl]
+ self._client_controls = None
+
+class Referrals(DSLdapObjects):
+ def __init__(self, instance, basedn, batch=False):
+ super(Referrals, self).__init__(instance, batch)
+ self._objectclasses = ['referral']
+ self._filterattrs = 'cn'
+ self._childobject = Referral
+ self._basedn = basedn
+ managedsait_ctrl = ldap.controls.simple.ManageDSAITControl()
+ self._server_controls = [managedsait_ctrl]
+ self._client_controls = None
+
diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py
new file mode 100644
index 0000000..cdd0a97
--- /dev/null
+++ b/src/lib389/lib389/replica.py
@@ -0,0 +1,1452 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+import os
+import decimal
+import time
+from lib389._constants import *
+from lib389.properties import *
+from lib389.utils import normalizeDN, escapeDNValue, ensure_bytes
+from lib389._replication import RUV
+from lib389.repltools import ReplTools
+from lib389 import DirSrv, Entry, NoSuchEntryError, InvalidArgumentError
+from lib389._mapped_object import DSLdapObjects, DSLdapObject
+
+
+class ReplicaLegacy(object):
+ proxied_methods = 'search_s getEntry'.split()
+
+ def __init__(self, conn):
+ """@param conn - a DirSrv instance"""
+ self.conn = conn
+ self.log = conn.log
+
+ def __getattr__(self, name):
+ if name in ReplicaLegacy.proxied_methods:
+ return DirSrv.__getattr__(self.conn, name)
+
+ def _get_mt_entry(self, suffix):
+ """Return the replica dn of the given suffix."""
+ mtent = self.conn.mappingtree.list(suffix=suffix)[0]
+ return ','.join(("cn=replica", mtent.dn))
+
+ @staticmethod
+ def _valid_role(role):
+ if role != ReplicaRole.MASTER and \
+ role != ReplicaRole.HUB and \
+ role != ReplicaRole.CONSUMER:
+ return False
+ else:
+ return True
+
+ @staticmethod
+ def _valid_rid(role, rid=None):
+ if role == ReplicaRole.MASTER:
+ if not decimal.Decimal(rid) or \
+ (rid <= 0) or \
+ (rid >= CONSUMER_REPLICAID):
+ return False
+ else:
+ if rid and (rid != CONSUMER_REPLICAID):
+ return False
+ return True
+
+ @staticmethod
+ def _set_or_default(attribute, properties, default):
+ '''
+ If 'attribute' or '+attribute' or '-attribute' exist
in
+ 'properties' it does nothing. Else it set (ldap.MOD_REPLACE)
+ 'attribute' in 'properties'
+ to the 'default' value
+ '''
+ add_attribute = "+%s" % attribute
+ del_attribute = "-%s" % attribute
+ if attribute in properties or \
+ add_attribute in properties or \
+ del_attribute in properties:
+ return
+ properties[attribute] = default
+
+ def create_repl_manager(self, repl_manager_dn=None, repl_manager_pw=None):
+ '''
+ Create an entry that will be used to bind as replica manager.
+
+ @param repl_manager_dn - DN of the bind entry. If not provided use
+ the default one
+ @param repl_manager_pw - Password of the entry. If not provide use
+ the default one
+
+ @return None
+
+ @raise - KeyError if can not find valid values of Bind DN and Pwd
+ '''
+
+ # check the DN and PW
+ try:
+ repl_manager_dn = repl_manager_dn or \
+ defaultProperties[REPLICATION_BIND_DN]
+ repl_manager_pw = repl_manager_pw or \
+ defaultProperties[REPLICATION_BIND_PW]
+ if not repl_manager_dn or not repl_manager_pw:
+ raise KeyError
+ except KeyError:
+ if not repl_manager_pw:
+ self.log.warning("replica_createReplMgr: bind DN password " +
+ "not specified")
+ if not repl_manager_dn:
+ self.log.warning("replica_createReplMgr: bind DN not " +
+ "specified")
+ raise
+
+ # if the replication manager entry already exists, ust return
+ try:
+ entries = self.search_s(repl_manager_dn, ldap.SCOPE_BASE,
+ "objectclass=*")
+ if entries:
+ # it already exist, fine
+ return
+ except ldap.NO_SUCH_OBJECT:
+ pass
+
+ # ok it does not exist, create it
+ try:
+ attrs = {'nsIdleTimeout': '0',
+ 'passwordExpirationTime': '20381010000000Z'}
+ self.conn.setupBindDN(repl_manager_dn, repl_manager_pw, attrs)
+ except ldap.ALREADY_EXISTS:
+ self.log.warn("User already exists (weird we just checked: %s " %
+ repl_manager_dn)
+
+ def list(self, suffix=None, replica_dn=None):
+ """
+ Return a list of replica entries.
+ If 'replica_dn' is specified it returns the related entry.
+ If 'suffix' is specified it returns the replica that is configured
+ for that 'suffix'.
+ If both 'replica_dn' and 'suffix' are specified it returns
the
+ 'replica_dn' entry.
+ If none of them are specified, it returns all the replicas
+
+ @param suffix - suffix of a replica
+ @param replica_dn - DN of a replica
+
+ @return replica entries
+
+ @raise search exceptions: ldap.NO_SUCH_OBJECT, ..
+ """
+
+ # set base/filtr according to the arguments
+ if replica_dn:
+ base = replica_dn
+ filtr = "(objectclass=%s)" % REPLICA_OBJECTCLASS_VALUE
+ elif suffix:
+ base = DN_MAPPING_TREE
+ filtr = ("(&(objectclass=%s)(%s=%s))" %
+ (REPLICA_OBJECTCLASS_VALUE,
+ REPLICA_PROPNAME_TO_ATTRNAME[REPLICA_SUFFIX], suffix))
+ else:
+ base = DN_MAPPING_TREE
+ filtr = "(objectclass=%s)" % REPLICA_OBJECTCLASS_VALUE
+
+ # now do the effective search
+ ents = self.conn.search_s(base, ldap.SCOPE_SUBTREE, filtr)
+ return ents
+
+ def setProperties(self, suffix=None, replica_dn=None, replica_entry=None,
+ properties=None):
+ '''
+ Set the properties of the replica. If an 'replica_entry' (Entry) is
+ provided, it updates the entry, else it updates the entry on the
+ server. If the 'replica_dn' is provided it retrieves the entry
+ using it, else it retrieve the replica using the 'suffix'.
+
+ @param suffix : suffix stored in that replica (online update)
+ @param replica_dn: DN of the replica (online update)
+ @param replica_entry: Entry of a replica (offline update)
+ @param properties: dictionary of properties
+ Supported properties are:
+ REPLICA_SUFFIX
+ REPLICA_ID
+ REPLICA_TYPE
+ REPLICA_BINDDN
+ REPLICA_PURGE_DELAY
+ REPLICA_PRECISE_PURGING
+ REPLICA_REFERRAL
+ REPLICA_FLAGS
+
+ @return None
+
+ @raise ValueError: if unknown properties
+ ValueError: if invalid replica_entry
+ ValueError: if replica_dn or suffix are not associated to
+ a replica
+
+ '''
+
+ # No properties provided
+ if len(properties) == 0:
+ return
+
+ # check that the given properties are valid
+ for prop in properties:
+ # skip the prefix to add/del value
+ if not inProperties(prop, REPLICA_PROPNAME_TO_ATTRNAME):
+ raise ValueError("unknown property: %s" % prop)
+ else:
+ self.log.debug("setProperties: %s:%s" %
+ (prop, properties[prop]))
+
+ # At least we need to have suffix/replica_dn/replica_entry
+ if not suffix and not replica_dn and not replica_entry:
+ raise InvalidArgumentError("suffix and replica_dn and replica_" +
+ "entry are missing")
+
+ # the caller provides a set of properties to set into a replica entry
+ if replica_entry:
+ if not isinstance(replica_entry, Entry):
+ raise ValueError("invalid instance of the replica_entry")
+
+ # that is fine, now set the values
+ for prop in properties:
+ val = rawProperty(prop)
+
+ # for Entry update it is a replace
+ replica_entry.update({REPLICA_PROPNAME_TO_ATTRNAME[val]:
+ properties[prop]})
+
+ return
+
+ # If it provides the suffix or the replicaDN, replica.list will
+ # return the appropriate entry
+ ents = self.conn.replica.list(suffix=suffix, replica_dn=replica_dn)
+ if len(ents) != 1:
+ if replica_dn:
+ raise ValueError("invalid replica DN: %s" % replica_dn)
+ else:
+ raise ValueError("invalid suffix: %s" % suffix)
+
+ # build the MODS
+ mods = []
+ for prop in properties:
+ # take the operation type from the property name
+ val = rawProperty(prop)
+ if str(prop).startswith('+'):
+ op = ldap.MOD_ADD
+ elif str(prop).startswith('-'):
+ op = ldap.MOD_DELETE
+ else:
+ op = ldap.MOD_REPLACE
+
+ mods.append((op, REPLICA_PROPNAME_TO_ATTRNAME[val],
+ properties[prop]))
+
+ # that is fine now to apply the MOD
+ self.conn.modify_s(ents[0].dn, mods)
+
+ def getProperties(self, suffix=None, replica_dn=None, replica_entry=None,
+ properties=None):
+ raise NotImplementedError
+
+ def get_role(self, suffix):
+ """Return the replica role
+
+ @return: ReplicaRole.MASTER, ReplicaRole.HUB, ReplicaRole.CONSUMER
+ """
+
+ filter_str =
('(&(objectclass=nsDS5Replica)(nsDS5ReplicaRoot=%s))'.format(suffix))
+
+ try:
+ replica_entry = self.conn.search_s(DN_CONFIG, ldap.SCOPE_SUBTREE,
+ filter_str)
+ if replica_entry:
+ repltype = replica_entry[0].getValue(REPL_TYPE)
+ replflags = replica_entry[0].getValue(REPL_FLAGS)
+
+ if repltype == REPLICA_RDWR_TYPE and replflags == REPLICA_FLAGS_WRITE:
+ replicarole = ReplicaRole.MASTER
+ elif repltype == REPLICA_RDONLY_TYPE and replflags ==
REPLICA_FLAGS_WRITE:
+ replicarole = ReplicaRole.HUB
+ elif repltype == REPLICA_RDONLY_TYPE and replflags ==
REPLICA_FLAGS_RDONLY:
+ replicarole = ReplicaRole.CONSUMER
+ else:
+ raise ValueError("Failed to determine a replica role")
+
+ return replicarole
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to get replica entry: %s' % str(e))
+
+ def create(self, suffix=None, role=None, rid=None, args=None):
+ """
+ Create a replica entry on an existing suffix.
+
+ @param suffix - dn of suffix
+ @param role - ReplicaRole.MASTER, ReplicaRole.HUB or
+ ReplicaRole.CONSUMER
+ @param rid - number that identify the supplier replica
+ (role=ReplicaRole.MASTER) in the topology. For
+ hub/consumer (role=ReplicaRole.HUB or
+ ReplicaRole.CONSUMER), rid value is not used.
+ This parameter is mandatory for supplier.
+
+ @param args - dictionary of initial replica's properties
+ Supported properties are:
+ REPLICA_SUFFIX
+ REPLICA_ID
+ REPLICA_TYPE
+ REPLICA_BINDDN [defaultProperties[REPLICATION_BIND_DN]]
+ REPLICA_PURGE_DELAY
+ REPLICA_PRECISE_PURGING
+ REPLICA_REFERRAL
+ REPLICA_FLAGS
+
+ @return replica DN
+
+ @raise InvalidArgumentError - if missing mandatory arguments
+ ValueError - argument with invalid value
+ LDAPError - failed to add replica entry
+
+ """
+ # Check validity of role
+ if not role:
+ self.log.fatal("Replica.create: replica role is not specified
(ReplicaRole.*)")
+ raise InvalidArgumentError("role missing")
+
+ if not ReplicaLegacy._valid_role(role):
+ self.log.fatal("enableReplication: replica role invalid (%s) " %
role)
+ raise ValueError("invalid role: %s" % role)
+
+ # check the validity of 'rid'
+ if not ReplicaLegacy._valid_rid(role, rid=rid):
+ self.log.fatal("Replica.create: replica role is master but
'rid'" +
+ " is missing or invalid value")
+ raise InvalidArgumentError("rid missing or invalid value")
+
+ # check the validity of the suffix
+ if not suffix:
+ self.log.fatal("Replica.create: suffix is missing")
+ raise InvalidArgumentError("suffix missing")
+ else:
+ nsuffix = normalizeDN(suffix)
+
+ # role is fine, set the replica type
+ if role == ReplicaRole.MASTER:
+ rtype = REPLICA_RDWR_TYPE
+ else:
+ rtype = REPLICA_RDONLY_TYPE
+
+ # Set the properties provided as mandatory parameter
+ # The attribute name is not prefixed '+'/'-' =>
ldap.MOD_REPLACE
+ properties = {REPLICA_SUFFIX: nsuffix,
+ REPLICA_ID: str(rid),
+ REPLICA_TYPE: str(rtype)}
+
+ # If the properties in args are valid
+ # add them to the 'properties' dictionary
+ # The attribute name may be prefixed '+'/'-' => keep MOD type
as
+ # provided in args
+ if args:
+ for prop in args:
+ if not inProperties(prop, REPLICA_PROPNAME_TO_ATTRNAME):
+ raise ValueError("unknown property: %s" % prop)
+ properties[prop] = args[prop]
+
+ # Now set default values of unset properties
+ ReplicaLegacy._set_or_default(REPLICA_BINDDN, properties,
+ [defaultProperties[REPLICATION_BIND_DN]])
+
+ # Set flags explicitly, so it will be more readable
+ if role == ReplicaRole.CONSUMER:
+ properties[REPLICA_FLAGS] = str(REPLICA_FLAGS_RDONLY)
+ else:
+ properties[REPLICA_FLAGS] = str(REPLICA_FLAGS_WRITE)
+
+ #
+ # Check if replica entry is already in the mapping-tree
+ #
+ mtents = self.conn.mappingtree.list(suffix=nsuffix)
+ mtent = mtents[0]
+ dn_replica = ','.join((RDN_REPLICA, mtent.dn))
+ try:
+ entry = self.conn.getEntry(dn_replica, ldap.SCOPE_BASE)
+ self.log.warn("Already setup replica for suffix %r" % nsuffix)
+ self.conn.suffixes.setdefault(nsuffix, {})
+ self.conn.replica.setProperties(replica_dn=dn_replica,
+ properties=properties)
+ return dn_replica
+ except ldap.NO_SUCH_OBJECT:
+ entry = None
+
+ #
+ # Now create the replica entry
+ #
+ entry = Entry(dn_replica)
+ entry.setValues("objectclass", "top",
"nsDS5Replica",
+ "extensibleobject")
+ self.conn.replica.setProperties(replica_entry=entry,
+ properties=properties)
+ self.conn.add_s(entry)
+ self.conn.suffixes[nsuffix] = {'dn': dn_replica, 'type': rtype}
+
+ return dn_replica
+
+ def deleteAgreements(self, suffix=None):
+ '''
+ Delete all the agreements for the suffix
+ '''
+ # check the validity of the suffix
+ if not suffix:
+ self.log.fatal("disableReplication: suffix is missing")
+ raise InvalidArgumentError("suffix missing")
+ else:
+ nsuffix = normalizeDN(suffix)
+
+ # Build the replica config DN
+ mtents = self.conn.mappingtree.list(suffix=nsuffix)
+ mtent = mtents[0]
+ dn_replica = ','.join((RDN_REPLICA, mtent.dn))
+
+ # Delete the agreements
+ try:
+ agmts = self.conn.agreement.list(suffix=suffix)
+ for agmt in agmts:
+ try:
+ self.conn.delete_s(agmt.dn)
+ except ldap.LDAPError as e:
+ self.log.fatal('Failed to delete replica agreement (%s),' +
+ ' error: %s' %
+ (agmt.dn, str(e)))
+ raise
+ except ldap.LDAPError as e:
+ self.log.fatal('Failed to search for replication agreements ' +
+ 'under (%s), error: %s' % (dn_replica, str(e)))
+ raise
+
+ def disableReplication(self, suffix=None):
+ '''
+ Delete a replica related to the provided suffix.
+ If this replica role was ReplicaRole.HUB or ReplicaRole.MASTER, it
+ also deletes the changelog associated to that replica. If it
+ exists some replication agreement below that replica, they are
+ deleted.
+
+ @param suffix - dn of suffix
+ @return None
+ @raise InvalidArgumentError - if suffix is missing
+ ldap.LDAPError - for all other update failures
+
+ '''
+
+ # check the validity of the suffix
+ if not suffix:
+ self.log.fatal("disableReplication: suffix is missing")
+ raise InvalidArgumentError("suffix missing")
+ else:
+ nsuffix = normalizeDN(suffix)
+
+ # Build the replica config DN
+ mtents = self.conn.mappingtree.list(suffix=nsuffix)
+ mtent = mtents[0]
+ dn_replica = ','.join((RDN_REPLICA, mtent.dn))
+
+ # Delete the agreements
+ try:
+ self.deleteAgreements(nsuffix)
+ except ldap.LDAPError as e:
+ self.log.fatal('Failed to delete replica agreements!')
+ raise
+
+ # Delete the replica
+ try:
+ self.conn.delete_s(dn_replica)
+ except ldap.LDAPError as e:
+ self.log.fatal('Failed to delete replica configuration ' +
+ '(%s), error: %s' % (dn_replica, str(e)))
+ raise
+
+ def enableReplication(self, suffix=None, role=None,
+ replicaId=CONSUMER_REPLICAID,
+ properties=None):
+ if not suffix:
+ self.log.fatal("enableReplication: suffix not specified")
+ raise ValueError("suffix missing")
+
+ if not role:
+ self.log.fatal("enableReplication: replica role not specify " +
+ "(ReplicaRole.*)")
+ raise ValueError("role missing")
+
+ #
+ # Check the validity of the parameters
+ #
+
+ # First role and replicaID
+ if role != ReplicaRole.MASTER and \
+ role != ReplicaRole.HUB and \
+ role != ReplicaRole.CONSUMER:
+ self.log.fatal("enableReplication: replica role invalid (%s) " %
+ role)
+ raise ValueError("invalid role: %s" % role)
+
+ if role == ReplicaRole.MASTER:
+ # check the replicaId [1..CONSUMER_REPLICAID[
+ if not decimal.Decimal(replicaId) or \
+ (replicaId <= 0) or \
+ (replicaId >= CONSUMER_REPLICAID):
+ self.log.fatal("enableReplication: invalid replicaId (%s) "
+ "for a RW replica" % replicaId)
+ raise ValueError("invalid replicaId %d (expected [1.."
+ "CONSUMER_REPLICAID]" % replicaId)
+ elif replicaId != CONSUMER_REPLICAID:
+ # check the replicaId is CONSUMER_REPLICAID
+ self.log.fatal("enableReplication: invalid replicaId (%s) for a "
+ "Read replica (expected %d)" %
+ (replicaId, CONSUMER_REPLICAID))
+ raise ValueError("invalid replicaId: %d for HUB/CONSUMER "
+ "replicaId is CONSUMER_REPLICAID" % replicaId)
+
+ # Now check we have a suffix
+ entries_backend = self.conn.backend.list(suffix=suffix)
+ if not entries_backend:
+ self.log.fatal("enableReplication: unable to retrieve the " +
+ "backend for %s" % suffix)
+ raise ValueError("no backend for suffix %s" % suffix)
+
+ ent = entries_backend[0]
+ if normalizeDN(suffix) != normalizeDN(ent.getValue('nsslapd-suffix')):
+ self.log.warning("enableReplication: suffix (%s) and backend "
+ "suffix (%s) differs" %
+ (suffix, entries_backend[0].nsslapd - suffix))
+ pass
+
+ # Now prepare the bindDN property
+ if properties is None:
+ properties = {REPLICA_BINDDN:
+ defaultProperties.get(REPLICATION_BIND_DN, None)}
+ elif REPLICA_BINDDN not in properties:
+ properties[REPLICA_BINDDN] = \
+ defaultProperties.get(REPLICATION_BIND_DN, None)
+ if not properties[REPLICA_BINDDN]:
+ # weird, internal error we do not retrieve the default
+ # replication bind DN this replica will not be updatable
+ # through replication until the binddn property will be set
+ self.log.warning("enableReplication: binddn not provided and"
+
+ " default value unavailable")
+ pass
+
+ # First add the changelog if master/hub
+ if (role == ReplicaRole.MASTER) or (role == ReplicaRole.HUB):
+ self.conn.changelog.create()
+
+ # Second create the default replica manager entry if it does not exist
+ # it should not be called from here but for the moment I am unsure when
+ # to create it elsewhere
+ self.conn.replica.create_repl_manager()
+ # then enable replication
+ ret = self.conn.replica.create(suffix=suffix, role=role, rid=replicaId,
+ args=properties)
+
+ return ret
+
+ def check_init(self, agmtdn):
+ """Check that a total update has completed
+ @returns tuple - first element is done/not done, 2nd is no error/has
+ error
+ @param agmtdn - the agreement dn
+ """
+ done, hasError = False, 0
+ attrlist = ['cn',
+ 'nsds5BeginReplicaRefresh',
+ 'nsds5replicaUpdateInProgress',
+ 'nsds5ReplicaLastInitStatus',
+ 'nsds5ReplicaLastInitStart',
+ 'nsds5ReplicaLastInitEnd']
+ try:
+ entry = self.conn.getEntry(
+ agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist)
+ except NoSuchEntryError:
+ self.log.exception("Error reading status from agreement %r" %
+ agmtdn)
+ hasError = 1
+ else:
+ refresh = entry.nsds5BeginReplicaRefresh
+ inprogress = entry.nsds5replicaUpdateInProgress
+ status = entry.nsds5ReplicaLastInitStatus
+ if not refresh: # done - check status
+ if not status:
+ print("No status yet")
+ elif status.find(ensure_bytes("replica busy")) > -1:
+ print("Update failed - replica busy - status", status)
+ done = True
+ hasError = 2
+ elif status.find(ensure_bytes("Total update succeeded")) >
-1:
+ print("Update succeeded: status ", status)
+ done = True
+ elif inprogress.lower() == ensure_bytes('true'):
+ print("Update in progress yet not in progress: status ",
+ status)
+ else:
+ print("Update failed: status", status)
+ hasError = 1
+ done = True
+ elif self.verbose:
+ print("Update in progress: status", status)
+
+ return done, hasError
+
+ def start_and_wait(self, agmtdn):
+ """@param agmtdn - agreement dn"""
+ rc = self.start_async(agmtdn)
+ if not rc:
+ rc = self.wait_init(agmtdn)
+ if rc == 2: # replica busy - retry
+ rc = self.start_and_wait(agmtdn)
+ return rc
+
+ def wait_init(self, agmtdn):
+ """Initialize replication and wait for completion.
+ @oaram agmtdn - agreement dn
+ """
+ done = False
+ haserror = 0
+ while not done and not haserror:
+ time.sleep(1) # give it a few seconds to get going
+ done, haserror = self.check_init(agmtdn)
+ return haserror
+
+ def start_async(self, agmtdn):
+ """Initialize replication without waiting.
+ @param agmtdn - agreement dn
+ """
+ self.log.info("Starting async replication %s" % agmtdn)
+ mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')]
+ self.conn.modify_s(agmtdn, mod)
+
+ def keep_in_sync(self, agmtdn):
+ """
+ @param agmtdn -
+ """
+ self.log.info("Setting agreement for continuous replication")
+ raise NotImplementedError("Check nsds5replicaupdateschedule before " +
+ "writing!")
+
+ def ruv(self, suffix, tryrepl=False):
+ """return a replica update vector for the given suffix.
+
+ @param suffix - eg. 'o=netscapeRoot'
+
+ @raises NoSuchEntryError if missing
+ """
+ filt = "(&(nsUniqueID=%s)(objectclass=%s))" % (REPLICA_RUV_UUID,
+ REPLICA_OC_TOMBSTONE)
+ attrs = ['nsds50ruv', 'nsruvReplicaLastModified']
+ ents = self.conn.search_s(suffix, ldap.SCOPE_SUBTREE, filt, attrs)
+ ent = None
+ if ents and (len(ents) > 0):
+ ent = ents[0]
+ elif tryrepl:
+ self.log.warn("Could not get RUV from %r entry -" +
+ " trying cn=replica" % suffix)
+ ensuffix = escapeDNValue(normalizeDN(suffix))
+ dn = ','.join(("cn=replica", "cn=%s" % ensuffix,
DN_MAPPING_TREE))
+ ents = self.conn.search_s(dn, ldap.SCOPE_BASE, "objectclass=*",
+ attrs)
+
+ if ents and (len(ents) > 0):
+ ent = ents[0]
+ self.log.debug("RUV entry is %r" % ent)
+ return RUV(ent)
+
+ raise NoSuchEntryError("RUV not found: suffix: %r" % suffix)
+
+ def promote(self, suffix, newrole, rid=None, binddn=None):
+ """
+ Promote the replica
+
+ @raise ValueError
+ """
+
+ if newrole != ReplicaRole.MASTER and newrole != ReplicaRole.HUB:
+ raise ValueError('Can only prompt replica to "master" or
"hub"')
+
+ if not binddn:
+ raise ValueError('"binddn" required for promotion')
+
+ if newrole == ReplicaRole.MASTER:
+ if not rid:
+ raise ValueError('"rid" required for promotion')
+ else:
+ # Must be a hub - set the rid
+ rid = CONSUMER_REPLICAID
+
+ # Get replica entry
+ filter_str = ('(&(objectclass=nsDS5Replica)(nsDS5ReplicaRoot=%s))' %
+ suffix)
+ try:
+ replica_entry = self.conn.search_s(DN_CONFIG, ldap.SCOPE_SUBTREE,
+ filter_str)
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to get replica entry: %s' % str(e))
+
+ # Check the role type
+ replicarole = self.get_role(suffix)
+
+ if newrole.value < replicarole.value:
+ raise ValueError('Can not promote replica to lower role: {} ->
{}'.format(replicarole.name, newrole.name))
+
+ #
+ # Create the changelog
+ #
+ try:
+ self.conn.changelog.create()
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to create changelog: %s' % str(e))
+
+ #
+ # Check that a RID was provided, and its a valid number
+ #
+ if newrole == ReplicaRole.MASTER:
+ try:
+ rid = int(rid)
+ except:
+ # Not a number
+ raise ValueError('"rid" value (%s) is not a number' %
str(rid))
+
+ if rid < 1 and rid > 65534:
+ raise ValueError('"rid" value (%d) is not in range ' +
+ ' 1 - 65534' % rid)
+
+ #
+ # Set bind dn
+ #
+ try:
+ self.conn.modify_s(replica_entry[0].dn, [(ldap.MOD_REPLACE,
+ REPL_BINDDN, binddn)])
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to update replica: ' + str(e))
+
+ #
+ # Set the replica type and flags
+ #
+ if newrole == ReplicaRole.HUB:
+ try:
+ self.conn.modify_s(replica_entry[0].dn,
+ [(ldap.MOD_REPLACE, REPL_TYPE, '2'),
+ (ldap.MOD_REPLACE, REPL_FLAGS, '1')])
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to update replica: ' + str(e))
+ else: # master
+ try:
+ self.conn.modify_s(replica_entry[0].dn,
+ [(ldap.MOD_REPLACE, REPL_TYPE, '3'),
+ (ldap.MOD_REPLACE, REPL_FLAGS, '1'),
+ (ldap.MOD_REPLACE, REPL_ID, str(rid))])
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to update replica: ' + str(e))
+
+ def demote(self, suffix, newrole):
+ """
+ Demote a replica to a hub or consumer
+
+ @raise ValueError
+ """
+ if newrole != ReplicaRole.CONSUMER and newrole != ReplicaRole.HUB:
+ raise ValueError('Can only demote replica to "hub" or
"consumer"')
+
+ # Get replica entry
+ filter_str = ('(&(objectclass=nsDS5Replica)(nsDS5ReplicaRoot=%s))' %
+ suffix)
+ try:
+ replica_entry = self.conn.search_s(DN_CONFIG, ldap.SCOPE_SUBTREE,
+ filter_str)
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to get replica entry: %s' % str(e))
+
+ # Check the role type
+ replicarole = self.get_role(suffix)
+
+ if newrole.value > replicarole.value:
+ raise ValueError('Can not demote replica to higher role: {} ->
{}'.format(replicarole.name, newrole.name))
+
+ #
+ # Demote it - set the replica type and flags
+ #
+ if newrole == ReplicaRole.HUB:
+ flag = '1'
+ else:
+ flag = '0'
+ try:
+ self.conn.modify_s(replica_entry[0].dn,
+ [(ldap.MOD_REPLACE, REPL_TYPE, '2'),
+ (ldap.MOD_REPLACE, REPL_FLAGS, flag),
+ (ldap.MOD_REPLACE, REPL_ID,
+ str(CONSUMER_REPLICAID))])
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to update replica: ' + str(e))
+
+
+class Replica(DSLdapObject):
+ """Replica DSLdapObject with:
+ - must attributes = ['cn', 'nsDS5ReplicaType',
'nsDS5ReplicaRoot',
+ 'nsDS5ReplicaBindDN', 'nsDS5ReplicaId']
+ - RDN attribute is 'cn'
+ - There is one "replica" per backend
+
+ :param instance: A instance
+ :type instance: lib389.DirSrv
+ :param dn: Entry DN
+ :type dn: str
+ :param batch: Not implemented
+ :type batch: bool
+ """
+
+ def __init__(self, instance, dn=None, batch=False):
+ super(Replica, self).__init__(instance, dn, batch)
+ self._rdn_attribute = 'cn'
+ self._must_attributes = ['cn', REPL_TYPE,
+ REPL_ROOT, REPL_BINDDN, REPL_ID]
+
+ self._create_objectclasses = ['top', 'extensibleObject',
+ REPLICA_OBJECTCLASS_VALUE]
+ self._protected = False
+ self._suffix = None
+
+ @staticmethod
+ def _valid_role(role):
+ """Return True if role is valid
+
+ :param role: MASTER, HUB and CONSUMER
+ :type role: ReplicaRole
+ :returns: True if the role is a valid role object, otherwise return False
+ """
+
+ if role != ReplicaRole.MASTER and \
+ role != ReplicaRole.HUB and \
+ role != ReplicaRole.CONSUMER:
+ return False
+ else:
+ return True
+
+ @staticmethod
+ def _valid_rid(role, rid=None):
+ """Return True if rid is valid for the replica role
+
+ :param role: MASTER, HUB and CONSUMER
+ :type role: ReplicaRole
+ :param rid: Only needed if the role is a MASTER
+ :type rid: int
+ :returns: True is rid is valid, otherwise return False
+ """
+
+ if rid is None:
+ return False
+ if role == ReplicaRole.MASTER:
+ if not decimal.Decimal(rid) or \
+ (rid <= 0) or \
+ (rid >= CONSUMER_REPLICAID):
+ return False
+ else:
+ if rid and (rid != CONSUMER_REPLICAID):
+ return False
+ return True
+
+ def delete(self):
+ """Delete a replica related to the provided suffix.
+
+ If this replica role was ReplicaRole.HUB or ReplicaRole.MASTER, it
+ also deletes the changelog associated to that replica. If it
+ exists some replication agreement below that replica, they are
+ deleted.
+
+ :returns: None
+ :raises: - InvalidArgumentError - if suffix is missing
+ - ldap.LDAPError - for all other update failures
+ """
+
+ # Get the suffix
+ suffix = self.get_attr_val(REPL_ROOT)
+ if not suffix:
+ self.log.fatal("disableReplication: suffix is not defined")
+ raise InvalidArgumentError("suffix missing")
+
+ # Delete the agreements
+ try:
+ self.deleteAgreements()
+ except ldap.LDAPError as e:
+ self.log.fatal('Failed to delete replica agreements!')
+ raise e
+
+ # Delete the replica
+ try:
+ super(Replica, self).delete()
+ except ldap.LDAPError as e:
+ self.log.fatal('Failed to delete replica configuration ' +
+ '(%s), error: %s' % (self._dn, str(e)))
+ raise e
+
+ def deleteAgreements(self):
+ """Delete all the agreements for the suffix
+
+ :raises: LDAPError - If failing to delete or search for agreeme :type
binddn: strnts
+ """
+
+ # Delete the agreements
+ try:
+ suffix = self.get_attr_val(REPL_ROOT)
+ agmts = self._instance.agreement.list(suffix=suffix)
+ for agmt in agmts:
+ try:
+ self._instance.delete_s(agmt.dn)
+ except ldap.LDAPError as e:
+ self.log.fatal('Failed to delete replica agreement (%s),' +
+ ' error: %s' % (agmt.dn, str(e)))
+ raise e
+ except ldap.LDAPError as e:
+ self.log.fatal('Failed to search for replication agreements ' +
+ 'under (%s), error: %s' % (self._dn, str(e)))
+ raise e
+
+ def promote(self, newrole, binddn=None, rid=None):
+ """Promote the replica to hub or master
+
+ :param newrole: The new replication role for the replica: MASTER and HUB
+ :type newrole: ReplicaRole
+ :param binddn: The replication bind dn - only applied to master
+ :type binddn: str
+ :param rid: The replication ID, applies only to promotions to "master"
+ :type rid: int
+
+ :returns: None
+ :raises: ValueError - If replica is not promoted
+ """
+
+ if not binddn:
+ binddn = defaultProperties[REPLICATION_BIND_DN]
+
+ # Check the role type
+ replicarole = self.get_role()
+ if newrole.value <= replicarole.value:
+ raise ValueError('Can not promote replica to lower or the same role: {}
-> {}'.format(replicarole.name, newrole.name))
+
+ if newrole == ReplicaRole.MASTER:
+ if not rid:
+ raise ValueError('"rid" required for promotion')
+ else:
+ # Must be a hub - set the rid
+ rid = CONSUMER_REPLICAID
+
+ # Create the changelog
+ try:
+ self._instance.changelog.create()
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to create changelog: %s' % str(e))
+
+ # Check that a RID was provided, and its a valid number
+ if newrole == ReplicaRole.MASTER:
+ try:
+ rid = int(rid)
+ except:
+ # Not a number
+ raise ValueError('"rid" value (%s) is not a number' %
str(rid))
+
+ if rid < 1 and rid > 65534:
+ raise ValueError('"rid" value (%d) is not in range ' +
+ ' 1 - 65534' % rid)
+
+ # Set bind dn
+ try:
+ self.set(REPL_BINDDN, binddn)
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to update replica: ' + str(e))
+
+ # Promote it - set the replica type, flags and rid
+ if replicarole == ReplicaRole.CONSUMER and newrole == ReplicaRole.HUB:
+ try:
+ self.set(REPL_FLAGS, str(REPLICA_FLAGS_WRITE))
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to update replica: ' + str(e))
+ elif replicarole == ReplicaRole.CONSUMER and newrole == ReplicaRole.MASTER:
+ try:
+ self.apply_mods([(REPL_TYPE, str(REPLICA_RDWR_TYPE)),
+ (REPL_FLAGS, str(REPLICA_FLAGS_WRITE)),
+ (REPL_ID, str(rid))])
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to update replica: ' + str(e))
+ elif replicarole == ReplicaRole.HUB and newrole == ReplicaRole.MASTER:
+ try:
+ self.apply_mods([(REPL_TYPE, str(REPLICA_RDWR_TYPE)),
+ (REPL_ID, str(rid))])
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to update replica: ' + str(e))
+
+ def demote(self, newrole):
+ """Demote a replica to a hub or consumer
+
+ :param newrole: The new replication role for the replica: CONSUMER and HUB
+ :type newrole: ReplicaRole
+
+ :returns: None
+ :raises: ValueError - If replica is not demoted
+ """
+
+ # Check the role type
+ replicarole = self.get_role()
+ if newrole.value >= replicarole.value:
+ raise ValueError('Can not demote replica to higher or the same role: {}
-> {}'.format(replicarole.name, newrole.name))
+
+ # Demote it - set the replica type, flags and rid
+ if replicarole == ReplicaRole.MASTER and newrole == ReplicaRole.HUB:
+ try:
+ self.apply_mods([(REPL_TYPE, str(REPLICA_RDONLY_TYPE)),
+ (REPL_ID, str(CONSUMER_REPLICAID))])
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to update replica: ' + str(e))
+ elif replicarole == ReplicaRole.MASTER and newrole == ReplicaRole.CONSUMER:
+ try:
+ self.apply_mods([(REPL_TYPE, str(REPLICA_RDONLY_TYPE)),
+ (REPL_FLAGS, str(REPLICA_FLAGS_RDONLY)),
+ (REPL_ID, str(CONSUMER_REPLICAID))])
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to update replica: ' + str(e))
+ elif replicarole == ReplicaRole.HUB and newrole == ReplicaRole.CONSUMER:
+ try:
+ self.set(REPL_FLAGS, str(REPLICA_FLAGS_RDONLY))
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to update replica: ' + str(e))
+
+ def get_role(self):
+ """Return the replica role
+
+ :returns: ReplicaRole.MASTER, ReplicaRole.HUB, ReplicaRole.CONSUMER
+ """
+
+ repltype = self.get_attr_val_int(REPL_TYPE)
+ replflags = self.get_attr_val_int(REPL_FLAGS)
+
+ if repltype == REPLICA_RDWR_TYPE and replflags == REPLICA_FLAGS_WRITE:
+ replicarole = ReplicaRole.MASTER
+ elif repltype == REPLICA_RDONLY_TYPE and replflags == REPLICA_FLAGS_WRITE:
+ replicarole = ReplicaRole.HUB
+ elif repltype == REPLICA_RDONLY_TYPE and replflags == REPLICA_FLAGS_RDONLY:
+ replicarole = ReplicaRole.CONSUMER
+ else:
+ raise ValueError("Failed to determine a replica role")
+
+ return replicarole
+
+ def check_init(self, agmtdn):
+ """Check that a total update has completed
+
+ :param agmtdn: The agreement DN
+ :type agmtdn: str
+
+ :returns: A tuple - first element is done/not done, 2nd is no error/has error
+
+ THIS SHOULD BE IN THE NEW AGREEMENT CLASS
+ """
+
+ done, hasError = False, 0
+ attrlist = ['cn',
+ 'nsds5BeginReplicaRefresh',
+ 'nsds5replicaUpdateInProgress',
+ 'nsds5ReplicaLastInitStatus',
+ 'nsds5ReplicaLastInitStart',
+ 'nsds5ReplicaLastInitEnd']
+ try:
+ entry = self._instance.getEntry(
+ agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist)
+ except NoSuchEntryError:
+ self._log.exception("Error reading status from agreement
{}".format(agmtdn))
+ hasError = 1
+ else:
+ refresh = entry.nsds5BeginReplicaRefresh
+ inprogress = entry.nsds5replicaUpdateInProgress
+ status = entry.nsds5ReplicaLastInitStatus
+ if not refresh: # done - check status
+ if not status:
+ print("No status yet")
+ elif status.find("replica busy") > -1:
+ print("Update failed - replica busy - status", status)
+ done = True
+ hasError = 2
+ elif status.find("Total update succeeded") > -1:
+ print("Update succeeded: status ", status)
+ done = True
+ elif inprogress.lower() == 'true':
+ print("Update in progress yet not in progress: status ",
+ status)
+ else:
+ print("Update failed: status", status)
+ hasError = 1
+ done = True
+ elif self.verbose:
+ print("Update in progress: status", status)
+
+ return done, hasError
+
+ def wait_init(self, agmtdn):
+ """Initialize replication and wait for completion.
+
+ :param agmtdn: The agreement DN
+ :type agmtdn: str
+
+ :returns: 0 if the initialization is complete
+
+ THIS SHOULD BE IN THE NEW AGREEMENT CLASS
+ """
+
+ done = False
+ haserror = 0
+ while not done and not haserror:
+ time.sleep(1) # give it a few seconds to get going
+ done, haserror = self.check_init(agmtdn)
+ return haserror
+
+ def start_and_wait(self, agmtdn):
+ """Initialize an agreement and wait for it to complete
+
+ @param agmtdn - agreement dn
+ @return - 0 if successful
+ THIS SHOULD BE IN THE NEW AGREEMENT CLASS
+ """
+
+ rc = self.start_async(agmtdn)
+ if not rc:
+ rc = self.wait_init(agmtdn)
+ if rc == 2: # replica busy - retry
+ rc = self.start_and_wait(agmtdn)
+ return rc
+
+ def start_async(self, agmtdn):
+ """Initialize replication without waiting.
+
+ :param agmtdn: The agreement DN
+ :type agmtdn: str
+
+ :returns: None
+
+ THIS SHOULD BE IN THE NEW AGREEMENT CLASS
+ """
+
+ self._log.info("Starting async replication %s" % agmtdn)
+ mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')]
+ self._instance.modify_s(agmtdn, mod)
+
+ def get_ruv_entry(self):
+ """Return the database RUV entry
+
+ :returns: The database RUV entry
+ :raises: ValeuError - If suffix is not setup for replication
+ LDAPError - If there is a problem trying to search for the RUV
+ """
+
+ try:
+ entry = self._instance.search_s(self._suffix,
+ ldap.SCOPE_SUBTREE,
+ REPLICA_RUV_FILTER)
+ if entry:
+ return entry[0]
+ else:
+ raise ValueError('Suffix (%s) is not setup for replication' %
self._suffix)
+ except ldap.LDAPError as e:
+ raise e
+
+ def test(self, *replica_dirsrvs):
+ """Make a "dummy" update on the the replicated suffix,
and check
+ all the provided replicas to see if they received the update.
+
+ :param *replica_dirsrvs: DirSrv instance, DirSrv instance, ...
+ :type *replica_dirsrvs: list of DirSrv
+
+ :returns: True - if all servers have recevioed the update by this
+ replica, otherwise return False
+ :raises: LDAPError - when failing to update/search database
+ """
+
+ # Generate a unique test value
+ test_value = ('test replication from ' + self._instance.serverid +
+ ' to ' + replica_dirsrvs[0].serverid + ': ' +
+ str(int(time.time())))
+ self._instance.modify_s(self._suffix,
+ [(ldap.MOD_REPLACE, 'description', test_value)])
+
+ for replica in replica_dirsrvs:
+ loop = 0
+ replicated = False
+ while loop <= 30:
+ # Wait 60 seconds before giving up
+ try:
+
+ entry = replica.getEntry(self._suffix,
+ ldap.SCOPE_BASE,
+ '(objectclass=*)')
+ if entry.hasValue('description', test_value):
+ replicated = True
+ break
+ except ldap.LDAPError as e:
+ raise e
+ loop += 1
+ time.sleep(2) # Check the replica every 2 seconds
+ if not replicated:
+ self._log.error('Replication is not in sync with replica ' +
+ 'server (%s)' % replica.serverid)
+ return False
+
+ # All is good, remove the test mod from the suffix entry
+ self._instance.modify_s(self._suffix,
+ [(ldap.MOD_DELETE, 'description', test_value)])
+
+ return True
+
+
+class Replicas(DSLdapObjects):
+ """Replica DSLdapObjects for all replicas
+
+ :param instance: A instance
+ :type instance: lib389.DirSrv
+ :param batch: Not implemented
+ :type batch: bool
+ """
+
+ def __init__(self, instance, batch=False):
+ super(Replicas, self).__init__(instance=instance, batch=False)
+ self._objectclasses = [REPLICA_OBJECTCLASS_VALUE]
+ self._filterattrs = [REPL_ROOT]
+ self._childobject = Replica
+ self._basedn = DN_MAPPING_TREE
+
+ def get(self, selector=[], dn=None):
+ """Get a child entry (DSLdapObject, Replica, etc.) with dn or
selector
+ using a base DN and objectClasses of our object (DSLdapObjects, Replicas, etc.)
+ After getting the replica, update the replica._suffix parameter.
+
+ :param dn: DN of wanted entry
+ :type dn: str
+ :param selector: An additional filter to objectClasses, i.e.
'backend_name'
+ :type dn: str
+
+ :returns: A child entry
+ """
+
+ replica = super(Replicas, self).get(selector, dn)
+ if replica:
+ # Get and set the replica's suffix
+ replica._suffix = replica.get_attr_val(REPL_ROOT)
+ return replica
+
+ def enable(self, suffix, role, replicaID=None, args=None):
+ """Enable replication for this suffix
+
+ :param suffix: The suffix to enable replication for
+ :type suffix: str
+ :param role: MASTER, HUB and CONSUMER
+ :type role: ReplicaRole
+ :param replicaID: number that identify the supplier replica
+ (role=ReplicaRole.MASTER) in the topology.
+ For hub/consumer (role=ReplicaRole.HUB or
+ ReplicaRole.CONSUMER), rid value is not used.
+ This parameter is mandatory for supplier.
+ :type replicaID: int
+ :param args: A dictionary of additional replica properties
+ :type args: dict
+
+ :returns: Replica DSLdapObject
+ :raises: - InvalidArgumentError - if missing mandatory arguments
+ - ValueError - argument with invalid value
+ - LDAPError - failed to add replica entry
+ """
+
+ # Normalize the suffix
+ suffix = normalizeDN(suffix)
+
+ # Check validity of role
+ if not role:
+ self._log.fatal("Replica.create: replica role is not specified
(ReplicaRole.*)")
+ raise InvalidArgumentError("role missing")
+
+ if not Replica._valid_role(role):
+ self._log.fatal("enableReplication: replica role invalid (%s) " %
role)
+ raise ValueError("invalid role: %s" % role)
+
+ # role is fine, set the replica type
+ if role == ReplicaRole.MASTER:
+ rtype = REPLICA_RDWR_TYPE
+ # check the validity of 'rid'
+ if not Replica._valid_rid(role, rid=replicaID):
+ self._log.fatal("Replica.create: replica role is master but "
+
+ "'rid' is missing or invalid value")
+ raise InvalidArgumentError("rid missing or invalid value")
+ else:
+ rtype = REPLICA_RDONLY_TYPE
+
+ # Set the properties provided as mandatory parameter
+ properties = {'cn': 'replica',
+ REPL_ROOT: suffix,
+ REPL_ID: str(replicaID),
+ REPL_TYPE: str(rtype)}
+
+ # If the properties in args are valid add them to 'properties'
+ if args:
+ for prop in args:
+ if not inProperties(prop, REPLICA_PROPNAME_TO_ATTRNAME):
+ raise ValueError("unknown property: %s" % prop)
+ properties[prop] = args[prop]
+
+ # Set flags explicitly, so it will be more readable
+ if role == ReplicaRole.CONSUMER:
+ properties[REPL_FLAGS] = str(REPLICA_FLAGS_RDONLY)
+ else:
+ properties[REPL_FLAGS] = str(REPLICA_FLAGS_WRITE)
+
+ # Check if replica entry is already in the mapping-tree
+ try:
+ replica = self.get(suffix)
+ # Should we return an error, or just return the existing relica?
+ self._log.warn("Already setup replica for suffix %s" % suffix)
+ return replica
+ except ldap.NO_SUCH_OBJECT:
+ pass
+
+ # Create changelog
+ if (role == ReplicaRole.MASTER) or (role == ReplicaRole.HUB):
+ self._instance.changelog.create()
+
+ # Create the default replica manager entry if it does not exist
+ if REPL_BINDDN not in properties:
+ properties[REPL_BINDDN] = defaultProperties[REPLICATION_BIND_DN]
+ if REPLICATION_BIND_PW not in properties:
+ repl_pw = defaultProperties[REPLICATION_BIND_PW]
+ else:
+ repl_pw = properties[REPLICATION_BIND_PW]
+ # Remove this property so we don't add it to the replica entry
+ del properties[REPLICATION_BIND_PW]
+
+ ReplTools.createReplManager(self._instance,
+ repl_manager_dn=properties[REPL_BINDDN],
+ repl_manager_pw=repl_pw)
+
+ # Now create the replica entry
+ mtents = self._instance.mappingtree.list(suffix=suffix)
+ self._basedn = mtents[0].dn
+ replica = self.create(RDN_REPLICA, properties)
+ replica._suffix = suffix
+
+ return replica
+
+ def disable(self, suffix):
+ """Disable replication on the suffix specified
+
+ :param suffix: Replicated suffix to disable
+ :type suffix: str
+
+ :returns: None
+ :raises: ValueError is suffix is not being replicated
+ """
+
+ try:
+ replica = self.get(suffix)
+ except ldap.NO_SUCH_OBJECT:
+ raise ValueError('Suffix (%s) is not setup for replication' %
suffix)
+
+ role = replica.get_role()
+ if role in (ReplicaRole.MASTER, ReplicaRole.HUB):
+ self._instance.changelog.delete()
+
+ try:
+ replica.delete()
+ except ldap.LDAPError as e:
+ raise ValueError('Failed to disable replication for suffix ' +
+ '(%s) LDAP error (%s)' % (suffix, str(e)))
+
+ def promote(self, suffix, newrole, binddn=None, rid=None):
+ """Promote the replica to hub or master
+
+ :param newrole: The new replication role for the replica: MASTER and HUB
+ :type newrole: ReplicaRole
+ :param binddn: The replication bind dn - only applied to master
+ :type binddn: str
+ :param rid: The replication ID, applies only to promotions to "master"
+ :type rid: int
+
+ :returns: None
+ :raises: ValueError - If replica is not promoted
+ """
+
+ replica = self.get(suffix)
+ try:
+ replica = self.get(suffix)
+ except ldap.NO_SUCH_OBJECT:
+ raise ValueError('Suffix (%s) is not setup for replication' %
suffix)
+ replica.promote(newrole, binddn, rid)
+
+ def demote(self, suffix, newrole):
+ """Demote a replica to a hub or consumer
+
+ :param newrole: The new replication role for the replica: CONSUMER and HUB
+ :type newrole: ReplicaRole
+
+ :returns: None
+ :raises: ValueError - If replica is not demoted
+ """
+
+ replica = self.get(suffix)
+ try:
+ replica = self.get(suffix)
+ except ldap.NO_SUCH_OBJECT:
+ raise ValueError('Suffix (%s) is not setup for replication' %
suffix)
+ replica.demote(newrole)
+
+ def get_dn(self, suffix):
+ """Return the DN of the replica from cn=config, this is also
+ known as the mapping tree entry
+
+ :param suffix: The replication suffix to get the mapping tree DN
+ :type suffix: str
+
+ :returns: The DN of the replication entry from cn=config
+ """
+
+ try:
+ replica = self.get(suffix)
+ except ldap.NO_SUCH_OBJECT:
+ raise ValueError('Suffix (%s) is not setup for replication' %
suffix)
+ return replica._dn
+
+ def get_ruv_entry(self, suffix):
+ """Return the database RUV entry for the provided suffix
+
+ :returns: The database RUV entry
+ :raises: - ValeuError - If suffix is not setup for replication
+ - LDAPError - If there is a problem trying to search for the RUV
+ """
+
+ try:
+ replica = self.get(suffix)
+ except ldap.NO_SUCH_OBJECT:
+ raise ValueError('Suffix (%s) is not setup for replication' %
suffix)
+ return replica.get_ruv_entry()
+
+ def test(self, suffix, *replica_dirsrvs):
+ """Make a "dummy" update on the the replicated suffix,
and check
+ all the provided replicas to see if they received the update.
+
+ :param suffix: The replicated suffix we want to check
+ :type suffix: str
+ :param *replica_dirsrvs: DirSrv instance, DirSrv instance, ...
+ :type *replica_dirsrvs: list of DirSrv
+
+ :returns: True - if all servers have received the update by this
+ replica, otherwise return False
+ :raises: LDAPError - when failing to update/search database
+ """
+
+ try:
+ replica = self.get(suffix)
+ except ldap.NO_SUCH_OBJECT:
+ raise ValueError('Suffix (%s) is not setup for replication' %
suffix)
+ return replica.test(*replica_dirsrvs)
diff --git a/src/lib389/lib389/repltools.py b/src/lib389/lib389/repltools.py
new file mode 100644
index 0000000..180a37d
--- /dev/null
+++ b/src/lib389/lib389/repltools.py
@@ -0,0 +1,298 @@
+import os
+import os.path
+import re
+import subprocess
+import ldap
+import logging
+from lib389._constants import *
+from lib389.properties import *
+
+logging.getLogger(__name__).setLevel(logging.INFO)
+log = logging.getLogger(__name__)
+
+
+# Helper functions
+def _alphanum_key(s):
+ """Turn the string into a list of string and number
parts"""
+
+ return [int(c) if c.isdigit() else c for c in
+ re.split('([0-9]+)', s)]
+
+
+def smart_sort(str_list):
+ """Sort the given list in the way that humans expect.
+
+ :param str_list: A list of strings to sort
+ :type str_list: list
+ """
+
+ str_list.sort(key=_alphanum_key)
+
+
+def _getCSNTime(inst, csn):
+ """Take a CSN and get the access log timestamp in seconds
+
+ :param inst: An instance to check access log
+ :type inst: lib389.DirSrv
+ :param csn: A "csn" string that is used to find when the csn was logged in
+ the access log, and what time in seconds it was logged.
+ :type csn: str
+
+ :returns: The time is seconds that the operation was logged
+ """
+
+ op_line = inst.ds_access_log.match('.*csn=%s' % csn)
+ if op_line:
+ #vals = inst.ds_access_log.parse_line(op_line[0])
+ return inst.ds_access_log.get_time_in_secs(op_line[0])
+ else:
+ return None
+
+
+def _getCSNandTime(inst, line):
+ """Take the line and find the CSN from the inst's access logs
+
+ :param inst: An instance to check access log
+ :type inst: lib389.DirSrv
+ :param line: A "RESULT" line from the access log that contains a
"csn"
+ :type line: str
+
+ :returns: A tuple containing the "csn" value and the time in seconds when
+ it was logged.
+ """
+
+ op_line = inst.ds_access_log.match('.*%s.*' % line)
+ if op_line:
+ vals = inst.ds_access_log.parse_line(op_line[0])
+ op = vals['op']
+ conn = vals['conn']
+
+ # Now find the result line and CSN
+ result_line = inst.ds_access_log.match_archive(
+ '.*conn=%s op=%s RESULT.*' % (conn, op))
+
+ if result_line:
+ vals = inst.ds_access_log.parse_line(result_line[0])
+ if 'csn' in vals:
+ ts = inst.ds_access_log.get_time_in_secs(result_line[0])
+ return (vals['csn'], ts)
+
+ return (None, None)
+
+
+class ReplTools(object):
+ """Replication tools"""
+
+ @staticmethod
+ def checkCSNs(dirsrv_replicas, ignoreCSNs=None):
+ """Gather all the CSN strings from the access and verify all of
those
+ CSNs exist on all the other replicas.
+
+ :param dirsrv_replicas: A list of DirSrv objects. The list must begin
+ with master replicas
+ :type dirsrv_replicas: list of lib389.DirSrv
+ :param ignoreCSNs: An optional string of csns to be ignored if
+ the caller knows that some csns can differ eg.:
+ '57e39e72000000020000|vucsn-57e39e76000000030000'
+ :type ignoreCSNs: str
+
+ :returns: True if all the CSNs are present, otherwise False
+ """
+
+ csn_logs = []
+ csn_log_count = 0
+
+ for replica in dirsrv_replicas:
+ logdir = '%s*' % replica.ds_access_log._get_log_path()
+ outfile = '/tmp/csn' + str(csn_log_count)
+ csn_logs.append(outfile)
+ csn_log_count += 1
+ if ignoreCSNs:
+ cmd = ("grep csn= " + logdir +
+ " | awk '{print $10}' | egrep -v '" +
ignoreCSNs + "' | sort -u > " + outfile)
+ else:
+ cmd = ("grep csn= " + logdir +
+ " | awk '{print $10}' | sort -u > " +
outfile)
+ os.system(cmd)
+
+ # Set a side the first master log - we use this for our "diffing"
+ main_log = csn_logs[0]
+ csn_logs.pop(0)
+
+ # Now process the remaining csn logs
+ for csnlog in csn_logs:
+ cmd = 'diff %s %s' % (main_log, csnlog)
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
+ line = proc.stdout.readline()
+ if line != "" and line != "\n":
+ if not line.startswith("\\"):
+ log.fatal("We have a CSN mismatch between (%s vs %s): %s"
%
+ (main_log, csnlog, line))
+ return False
+
+ return True
+
+ @staticmethod
+ def replConvReport(suffix, ops, replica, all_replicas):
+ """Find and measure the convergence of entries from a replica,
and
+ print a report on how fast all the "ops" replicated to the other
+ replicas.
+
+ :param suffix: Replicated suffix
+ :type suffix: str
+ :param ops: a list of "operations" to search for in the access logs
+ :type ops: list
+ :param replica: Instance where the entries originated
+ :type replica: lib389.DirSrv
+ :param all_replicas: Suppliers, hubs, consumers
+ :type all_replicas: list of lib389.DirSrv
+
+ :returns: The longest time in seconds for an operation to fully converge
+ """
+ highest_time = 0
+ total_time = 0
+
+ print('Convergence Report for replica: %s (%s)' %
+ (replica.serverid, suffix))
+ print('-' * 80)
+
+ # Loop through each operation checking all the access logs
+ for op in ops:
+ csnstr, csntime = _getCSNandTime(replica, op)
+ if csnstr is None and csntime is None:
+ # Didn't find a csn, move on
+ continue
+
+ conv_time = []
+ longest_time = 0
+ for inst in all_replicas:
+ replObj = inst.replicas.get(suffix)
+ if replObj is None:
+ inst.log.warn('(%s) not setup for replication of (%s)' %
+ (inst.serverid, suffix))
+ continue
+ ctime = _getCSNTime(inst, csnstr)
+ if ctime:
+ role = replObj.get_role()
+ if role == ReplicaRole.MASTER:
+ txt = ' Master (%s)' % (inst.serverid)
+ elif role == ReplicaRole.HUB:
+ txt = ' Hub (%s)' % (inst.serverid)
+ elif role == ReplicaRole.CONSUMER:
+ txt = ' Consumer (%s)' % (inst.serverid)
+ else:
+ txt = '?'
+ ctime = ctime - csntime
+ conv_time.append(str(ctime) + txt)
+ if ctime > longest_time:
+ longest_time = ctime
+
+ smart_sort(conv_time)
+ print('\n Operation: %s\n %s' % (op, '-' * 40))
+ print('\n Convergence times:')
+ for line in conv_time:
+ parts = line.split(' ', 1)
+ print(' %8s secs - %s' % (parts[0], parts[1]))
+ print('\n Longest Convergence Time: ' +
+ str(longest_time))
+ if longest_time > highest_time:
+ highest_time = longest_time
+ total_time += longest_time
+
+ print('\n Summary for "{}"'.format(replica.serverid))
+ print(' ----------------------------------------')
+ print(' Highest convergence time: {} seconds'.format(highest_time))
+ print(' Average longest convergence time: {}
seconds\n'.format(int(total_time / len(ops))))
+
+ return highest_time
+
+ @staticmethod
+ def replIdle(replicas, suffix=DEFAULT_SUFFIX):
+ """Take a list of DirSrv Objects and check to see if all of the
present
+ replication agreements are idle for a particular backend
+
+ :param replicas: Suppliers, hubs, consumers
+ :type replicas: list of lib389.DirSrv
+ :param suffix: Replicated suffix
+ :type suffix: str
+
+ :raises: LDAPError: if unable to search for the replication agreements
+ :returns: True if all the agreements are idle, otherwise False
+ """
+
+ IDLE_MSG = ('Replica acquired successfully: Incremental ' +
+ 'update succeeded')
+ STATUS_ATTR = 'nsds5replicaLastUpdateStatus'
+ FILTER = ('(&(nsDS5ReplicaRoot=' + suffix +
+ ')(objectclass=nsds5replicationAgreement))')
+ repl_idle = True
+
+ for inst in replicas:
+ try:
+ entries = inst.search_s("cn=config",
+ ldap.SCOPE_SUBTREE, FILTER, [STATUS_ATTR])
+ if entries:
+ for entry in entries:
+ if IDLE_MSG not in entry.getValue(STATUS_ATTR):
+ repl_idle = False
+ break
+
+ if not repl_idle:
+ break
+
+ except ldap.LDAPError as e:
+ log.fatal('Failed to search the repl agmts on ' +
+ '%s - Error: %s' % (inst.serverid, str(e)))
+ assert False
+ return repl_idle
+
+ @staticmethod
+ def createReplManager(server, repl_manager_dn=None, repl_manager_pw=None):
+ """Create an entry that will be used to bind as replication
manager.
+
+ :param server: An instance to connect to
+ :type server: lib389.DirSrv
+ :param repl_manager_dn: DN of the bind entry. If not provided use
+ the default one
+ :type repl_manager_dn: str
+ :param repl_manager_pw: Password of the entry. If not provide use
+ the default one
+ :type repl_manager_pw: str
+
+ :returns: None
+ :raises: - KeyError - if can not find valid values of Bind DN and Pwd
+ - LDAPError - if we fail to add the replication manager
+ """
+
+ # check the DN and PW
+ try:
+ repl_manager_dn = repl_manager_dn or \
+ defaultProperties[REPLICATION_BIND_DN]
+ repl_manager_pw = repl_manager_pw or \
+ defaultProperties[REPLICATION_BIND_PW]
+ if not repl_manager_dn or not repl_manager_pw:
+ raise KeyError
+ except KeyError:
+ if not repl_manager_pw:
+ server.log.warning("replica_createReplMgr: bind DN password "
+
+ "not specified")
+ if not repl_manager_dn:
+ server.log.warning("replica_createReplMgr: bind DN not " +
+ "specified")
+ raise
+
+ # If the replication manager entry already exists, just return
+ try:
+ entries = server.search_s(repl_manager_dn, ldap.SCOPE_BASE,
+ "objectclass=*")
+ if entries:
+ # it already exist, fine
+ return
+ except ldap.NO_SUCH_OBJECT:
+ pass
+
+ # ok it does not exist, create it
+ attrs = {'nsIdleTimeout': '0',
+ 'passwordExpirationTime': '20381010000000Z'}
+ server.setupBindDN(repl_manager_dn, repl_manager_pw, attrs)
+
diff --git a/src/lib389/lib389/rootdse.py b/src/lib389/lib389/rootdse.py
new file mode 100644
index 0000000..efd299f
--- /dev/null
+++ b/src/lib389/lib389/rootdse.py
@@ -0,0 +1,47 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import ldap
+from lib389._constants import *
+from lib389 import Entry
+from lib389._mapped_object import DSLdapObject
+
+
+class RootDSE(DSLdapObject):
+ """
+ Check if the directory supports features or not.
+ """
+ def __init__(self, conn, batch=False):
+ """@param conn - a DirSrv instance """
+ super(RootDSE, self).__init__(instance=conn, batch=batch)
+ self._dn = ""
+
+ def supported_sasl(self):
+ return self.get_attr_vals('supportedSASLMechanisms')
+
+ def supports_sasl_gssapi(self):
+ return self.present("supportedSASLMechanisms", 'GSSAPI')
+
+ def supports_sasl_ldapssotoken(self):
+ return self.present("supportedSASLMechanisms",
"LDAPSSOTOKEN")
+
+ def supports_sasl_plain(self):
+ return self.present("supportedSASLMechanisms", "PLAIN")
+
+ def supports_sasl_external(self):
+ return self.present("supportedSASLMechanisms", "EXTERNAL")
+
+ def supports_exop_whoami(self):
+ return self.present("supportedExtension",
"1.3.6.1.4.1.4203.1.11.3")
+
+ def supports_exop_ldapssotoken_request(self):
+ return self.present("supportedExtension",
"2.16.840.1.113730.3.5.14")
+
+ def supports_exop_ldapssotoken_revoke(self):
+ return self.present("supportedExtension",
"2.16.840.1.113730.3.5.16")
+
diff --git a/src/lib389/lib389/sasl.py b/src/lib389/lib389/sasl.py
new file mode 100644
index 0000000..0015f60
--- /dev/null
+++ b/src/lib389/lib389/sasl.py
@@ -0,0 +1,36 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""
+Lib389 python ldap sasl operations.
+
+These should be upstreamed if possible.
+"""
+
+from ldap.sasl import sasl, CB_AUTHNAME, CB_PASS, CB_USER
+
+from lib389.utils import ensure_bytes
+
+class LdapSSOTokenSASL(sasl):
+ """
+ This class handles draft-wibrown-ldapssotoken-01 authentication.
+ """
+
+ def __init__(self,token):
+ auth_dict = { CB_PASS:token }
+ sasl.__init__(self, auth_dict, "LDAPSSOTOKEN")
+
+class PlainSASL(sasl):
+ """
+ This class handles PLAIN sasl authentication
+ """
+
+ def __init__(self, authz_id, passwd):
+ auth_dict = { CB_AUTHNAME:authz_id, CB_PASS:passwd }
+ sasl.__init__(self, auth_dict, "PLAIN")
+
diff --git a/src/lib389/lib389/schema.py b/src/lib389/lib389/schema.py
new file mode 100755
index 0000000..6003093
--- /dev/null
+++ b/src/lib389/lib389/schema.py
@@ -0,0 +1,236 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""
+ You will access this from:
+ DirSrv.schema.methodName()
+"""
+import glob
+import ldap
+from ldap.schema.models import AttributeType, ObjectClass, MatchingRule
+
+from lib389._constants import *
+from lib389._constants import DN_SCHEMA
+from lib389.utils import ds_is_newer
+from lib389._mapped_object import DSLdapObject
+from lib389.tasks import SchemaReloadTask
+
+
+class Schema(DSLdapObject):
+ def __init__(self, instance, batch=False):
+ super(Schema, self).__init__(instance=instance, batch=batch)
+ self._dn = DN_SCHEMA
+ self._rdn_attribute = 'cn'
+
+ def reload(self, schema_dir=None):
+ task = SchemaReloadTask(self._instance)
+
+ task_properties = {}
+ if schema_dir is not None:
+ task_properties['schemadir'] = schema_dir
+
+ task.create(properties=task_properties)
+
+ return task
+
+
+class SchemaLegacy(object):
+
+ def __init__(self, conn):
+ """@param conn - a DirSrv instance"""
+ self.conn = conn
+ self.log = conn.log
+
+ def get_entry(self):
+ """get the schema as an LDAP entry"""
+ attrs = ['attributeTypes', 'objectClasses']
+ return self.conn.search_s(DN_SCHEMA, ldap.SCOPE_BASE,
+ 'objectclass=*', attrs)[0]
+
+ def get_subschema(self):
+ """get the schema as a python-ldap SubSchema
object"""
+ return ldap.schema.SubSchema(self.get_entry().data)
+
+ def list_files(self):
+ """return a list of the schema files in the instance
schemadir"""
+ file_list = []
+ file_list += glob.glob(self.conn.schemadir + "/*.ldif")
+ if ds_is_newer('1.3.6.0'):
+ file_list += glob.glob(self.conn.ds_paths.system_schema_dir +
"/*.ldif")
+ return file_list
+
+ def file_to_ldap(self, filename):
+ """convert the given schema file name to its python-ldap format
+ suitable for passing to ldap.schema.SubSchema()
+ @param filename - the full path and filename of a schema file in ldif
+ format"""
+ import six.moves.urllib.request
+ import six.moves.urllib.parse
+ import ldif
+
+ ldif_file = six.moves.urllib.request.urlopen('file://' + filename)
+ ldif_parser = ldif.LDIFRecordList(ldif_file, max_entries=1)
+ if not ldif_parser:
+ return None
+ ldif_parser.parse()
+ if not ldif_parser.all_records:
+ return None
+ return ldif_parser.all_records[0][1]
+
+ def file_to_subschema(self, filename):
+ """convert the given schema file name to its python-ldap format
+ ldap.schema.SubSchema object
+ @param filename - the full path and filename of a schema file in ldif
+ format"""
+ ent = self.file_to_ldap(filename)
+ if not ent:
+ return None
+ return ldap.schema.SubSchema(ent)
+
+ def add_schema(self, attr, val):
+ """Add a schema element to the schema.
+ @param attr - the attribute type to use e.g. attributeTypes or
+ objectClasses
+ @param val the schema element definition to add"""
+ self.conn.modify_s(DN_SCHEMA, [(ldap.MOD_ADD, attr, val)])
+
+ def del_schema(self, attr, val):
+ """Delete a schema element from the schema.
+ @param attr - the attribute type to use e.g. attributeTypes or
+ objectClasses
+ @param val - the schema element definition to delete"""
+ self.conn.modify_s(DN_SCHEMA, [(ldap.MOD_DELETE, attr, val)])
+
+ def add_attribute(self, *attributes):
+ """Add an attribute type definition to the schema.
+ @param attributes a single or list of attribute type defintions to add
+ """
+ return self.add_schema('attributeTypes', attributes)
+
+ def add_objectclass(self, *objectclasses):
+ """Add an object class definition to the schema.
+ @param objectclasses a single or list of object class defintions to add
+ """
+ return self.add_schema('objectClasses', objectclasses)
+
+ def get_schema_csn(self):
+ """return the schema nsSchemaCSN attribute"""
+ ents = self.conn.search_s(DN_SCHEMA, ldap.SCOPE_BASE,
+ "objectclass=*", ['nsSchemaCSN'])
+ ent = ents[0]
+ return ent.getValue('nsSchemaCSN')
+
+ def get_objectclasses(self):
+ """Returns a list of ldap.schema.models.ObjectClass objects for
all
+ objectClasses supported by this instance.
+ """
+ attrs = ['objectClasses']
+ results = self.conn.search_s(DN_SCHEMA, ldap.SCOPE_BASE,
+ 'objectclass=*', attrs)[0]
+ objectclasses = [ObjectClass(oc) for oc in
+ results.getValues('objectClasses')]
+ return objectclasses
+
+ def get_attributetypes(self):
+ """Returns a list of ldap.schema.models.AttributeType objects for
all
+ attributeTypes supported by this instance.
+ """
+ attrs = ['attributeTypes']
+ results = self.conn.search_s(DN_SCHEMA, ldap.SCOPE_BASE,
+ 'objectclass=*', attrs)[0]
+ attributetypes = [AttributeType(at) for at in
+ results.getValues('attributeTypes')]
+ return attributetypes
+
+ def get_matchingrules(self):
+ """Return a list of the server defined matching
rules"""
+ attrs = ['matchingrules']
+ results = self.conn.search_s(DN_SCHEMA, ldap.SCOPE_BASE,
+ 'objectclass=*', attrs)[0]
+ matchingRules = [MatchingRule(mr) for mr in
+ results.getValues('matchingRules')]
+ return matchingRules
+
+ def query_matchingrule(self, mr_name):
+ """Returns a single matching rule instance that matches the
mr_name.
+ Returns None if the matching rule doesn't exist.
+
+ @param mr_name - The name of the matching rule you want to query.
+
+ return MatchingRule or None
+
+ <ldap.schema.models.MatchingRule instance>
+ """
+ matchingRules = self.get_matchingrules()
+ matchingRule = [mr for mr in matchingRules if mr_name.lower() in
+ list(map(str.lower, mr.names))]
+ if len(matchingRule) != 1:
+ # This is an error.
+ return None
+ matchingRule = matchingRule[0]
+ return matchingRule
+
+ def query_objectclass(self, objectclassname):
+ """Returns a single ObjectClass instance that matches
objectclassname.
+ Returns None if the objectClass doesn't exist.
+
+ @param objectclassname - The name of the objectClass you want to query.
+
+ return ObjectClass or None
+
+ ex. query_objectclass('account')
+ <ldap.schema.models.ObjectClass instance>
+ """
+ objectclasses = self.get_objectclasses()
+
+ objectclass = [oc for oc in objectclasses if objectclassname.lower() in
+ list(map(str.lower, oc.names))]
+ if len(objectclass) != 1:
+ # This is an error.
+ return None
+ objectclass = objectclass[0]
+ return objectclass
+
+ def query_attributetype(self, attributetypename):
+ """Returns a tuple of the AttributeType, and what objectclasses
may or
+ must take this attributeType. Returns None if attributetype doesn't
+ exist.
+
+ @param attributetypename - The name of the attributeType you want to
+ query
+
+ return (AttributeType, Must, May) or None
+
+ ex. query_attributetype('uid')
+ ( <ldap.schema.models.AttributeType instance>,
+ [<ldap.schema.models.ObjectClass instance>, ...],
+ [<ldap.schema.models.ObjectClass instance>, ...] )
+ """
+ # First, get the attribute that matches name. We need to consider
+ # alternate names. There is no way to search this, so we have to
+ # filter our set of all attribute types.
+ objectclasses = self.get_objectclasses()
+ attributetypes = self.get_attributetypes()
+ attributetypename = attributetypename.lower()
+
+ attributetype = [at for at in attributetypes
+ if attributetypename.lower() in
+ list(map(str.lower, at.names))]
+ if len(attributetype) != 1:
+ # This is an error.
+ return None
+ attributetype = attributetype[0]
+ # Get the primary name of this attribute
+ attributetypename = attributetype.names[0]
+ # Build a set if they have may.
+ may = [oc for oc in objectclasses if attributetypename.lower() in
+ list(map(str.lower, oc.may))]
+ # Build a set if they have must.
+ must = [oc for oc in objectclasses if attributetypename.lower() in
+ list(map(str.lower, oc.must))]
+ return (attributetype, must, may)
diff --git a/src/lib389/lib389/suffix.py b/src/lib389/lib389/suffix.py
new file mode 100644
index 0000000..2ea759e
--- /dev/null
+++ b/src/lib389/lib389/suffix.py
@@ -0,0 +1,98 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389 import DirSrv, InvalidArgumentError
+from lib389.properties import *
+
+
+class Suffix(object):
+ proxied_methods = 'search_s getEntry'.split()
+
+ def __init__(self, conn):
+ """@param conn - a DirSrv instance"""
+ self.conn = conn
+ self.log = conn.log
+
+ def __getattr__(self, name):
+ if name in Suffix.proxied_methods:
+ return DirSrv.__getattr__(self.conn, name)
+
+ def list(self):
+ '''
+ Returns the list of suffixes DN for which it exists a mapping tree
+ entry
+
+ @param None
+
+ @return list of suffix DN
+
+ @raise None
+ '''
+ suffixes = []
+ ents = self.conn.mappingtree.list()
+ for ent in ents:
+ vals = self.conn.mappingtree.toSuffix(entry=ent)
+ suffixes.append(vals[0])
+ return suffixes
+
+ def toBackend(self, suffix=None):
+ '''
+ Returns the backend entry that stores the provided suffix
+
+ @param suffix - suffix DN of the backend
+
+ @return backend - LDAP entry of the backend
+
+ @return ValueError - if suffix is not provided
+
+ '''
+ if not suffix:
+ raise ValueError("suffix is mandatory")
+
+ return self.conn.backend.list(suffix=suffix)
+
+ def getParent(self, suffix=None):
+ '''
+ Returns the DN of a suffix that is the parent of the provided
+ 'suffix'.
+ If 'suffix' has no parent, it returns None
+
+ @param suffix - suffix DN of the backend
+
+ @return parent suffix DN
+
+ @return ValueError - if suffix is not provided
+ InvalidArgumentError - if suffix is not implemented on the
+ server
+
+ '''
+ if not suffix:
+ raise ValueError("suffix is mandatory")
+
+ ents = self.conn.mappingtree.list(suffix=suffix)
+ if len(ents) == 0:
+ raise InvalidArgumentError(
+ "suffix %s is not implemented on that server" % suffix)
+
+ mapping_tree = ents[0]
+ if mapping_tree.hasValue(MT_PROPNAME_TO_ATTRNAME[MT_PARENT_SUFFIX]):
+ return mapping_tree.getValue(
+ MT_PROPNAME_TO_ATTRNAME[MT_PARENT_SUFFIX])
+ else:
+ return None
+
+ raise NotImplemented
+
+ def setProperties(self, suffix):
+ '''
+ Supported properties:
+
+ '''
+
+ if not suffix:
+ raise ValueError("suffix is mandatory")
diff --git a/src/lib389/lib389/tasks.py b/src/lib389/lib389/tasks.py
new file mode 100644
index 0000000..44b191f
--- /dev/null
+++ b/src/lib389/lib389/tasks.py
@@ -0,0 +1,1195 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import time
+import os.path
+import ldap
+from datetime import datetime
+
+
+from lib389 import Entry
+from lib389._mapped_object import DSLdapObject
+from lib389.exceptions import Error
+from lib389._constants import (
+ DEFAULT_SUFFIX, DEFAULT_BENAME, DN_EXPORT_TASK, DN_BACKUP_TASK,
+ DN_IMPORT_TASK, DN_RESTORE_TASK, DN_INDEX_TASK, DN_MBO_TASK,
+ DN_TOMB_FIXUP_TASK, DN_TASKS, DIRSRV_STATE_ONLINE
+ )
+from lib389.properties import (
+ TASK_WAIT, EXPORT_REPL_INFO, MT_PROPNAME_TO_ATTRNAME, MT_SUFFIX,
+ TASK_TOMB_STRIP
+ )
+
+
+class Task(DSLdapObject):
+ def __init__(self, instance, dn=None, batch=False):
+ super(Task, self).__init__(instance, dn, batch)
+ self._rdn_attribute = 'cn'
+ self._must_attributes = ['cn']
+ self._create_objectclasses = ['top', 'extensibleObject']
+ self._protected = False
+ self._exit_code = None
+
+ def is_complete(self):
+ """Return True if task is complete, else False."""
+ self._exit_code = self.get_attr_val("nsTaskExitCode")
+ if not self.exists() or self._exit_code is not None:
+ return True
+ return False
+
+ def get_exit_code(self):
+ """Return task's exit code if task is complete, else
None."""
+ if self.is_complete():
+ try:
+ return int(self._exit_code)
+ except TypeError:
+ return None
+ return None
+
+ def wait(self):
+ """Wait until task is complete."""
+ while True:
+ if self.is_complete():
+ break
+ time.sleep(1)
+
+ def create(self, rdn=None, properties=None, basedn=None):
+ properties['cn'] = self.cn
+ return super(Task, self).create(rdn, properties, basedn)
+
+ @staticmethod
+ def _get_task_date():
+ """Return a timestamp to use in naming new task
entries."""
+ return datetime.now().strftime('%Y_%m_%d_%H_%M_%S')
+
+
+class MemberOfFixupTask(Task):
+ def __init__(self, instance, dn=None, batch=False):
+ self.cn = 'memberOf_fixup_' + Task._get_task_date()
+ dn = "cn=" + self.cn + "," + DN_MBO_TASK
+
+ super(MemberOfFixupTask, self).__init__(instance, dn, batch)
+ self._must_attributes.extend(['basedn'])
+
+
+class USNTombstoneCleanupTask(Task):
+ def __init__(self, instance, dn=None, batch=False):
+ self.cn = 'usn_cleanup_' + Task._get_task_date()
+ dn = "cn=" + self.cn + ",cn=USN tombstone cleanup task," +
DN_TASKS
+
+ super(USNTombstoneCleanupTask, self).__init__(instance, dn, batch)
+
+ def _validate(self, rdn, properties, basedn):
+ if not 'suffix' in properties and not 'backend' in properties:
+ raise Error("Either suffix or backend must be specified for cleanup
task.")
+
+ return super(USNTombstoneCleanupTask, self)._validate(rdn, properties, basedn)
+
+class SchemaReloadTask(Task):
+ def __init__(self, instance, dn=None, batch=False):
+ self.cn = 'schema_reload_' + Task._get_task_date()
+ dn = "cn=" + self.cn + ",cn=schema reload task," + DN_TASKS
+
+ super(SchemaReloadTask, self).__init__(instance, dn, batch)
+
+class Tasks(object):
+ proxied_methods = 'search_s getEntry'.split()
+
+ def __init__(self, conn):
+ """@param conn - a DirSrv instance"""
+ self.conn = conn
+ self.log = conn.log
+ self.dn = None # DN of the last task attempted
+ self.entry = None
+
+ def __getattr__(self, name):
+ from lib389 import DirSrv
+ if name in Tasks.proxied_methods:
+ return DirSrv.__getattr__(self.conn, name)
+
+ def checkTask(self, entry, dowait=False):
+ '''check task status - task is complete when the nsTaskExitCode attr
+ is set return a 2 tuple (true/false,code) first is false if task is
+ running, true if done - if true, second is the exit code - if dowait
+ is True, this function will block until the task is complete'''
+ attrlist = ['nsTaskLog', 'nsTaskStatus',
'nsTaskExitCode',
+ 'nsTaskCurrentItem', 'nsTaskTotalItems']
+ done = False
+ exitCode = 0
+ dn = entry.dn
+ while not done:
+ entry = self.conn.getEntry(dn, attrlist=attrlist)
+ self.log.debug("task entry %r" % entry)
+
+ if entry.nsTaskExitCode:
+ exitCode = int(entry.nsTaskExitCode)
+ done = True
+ if dowait:
+ time.sleep(1)
+ else:
+ break
+ return (done, exitCode)
+
+ def importLDIF(self, suffix=None, benamebase=None, input_file=None,
+ args=None):
+ '''
+ Import from a LDIF format a given 'suffix' (or 'benamebase' that
stores
+ that suffix). It uses an internal task to acheive this request.
+
+ If 'suffix' and 'benamebase' are specified, it uses
'benamebase' first
+ else 'suffix'.
+ If both 'suffix' and 'benamebase' are missing it raise
ValueError
+
+ 'input_file' is the ldif input file
+
+ @param suffix - suffix of the backend
+ @param benamebase - 'commonname'/'cn' of the backend (e.g.
'userRoot')
+ @param ldif_input - file that will contain the entries in LDIF format
+ to import
+ @param args - is a dictionary that contains modifier of the import task
+ wait: True/[False] - If True, 'export' waits for the completion
+ of the task before to return
+
+ @return None
+
+ @raise ValueError
+
+ '''
+ if self.conn.state != DIRSRV_STATE_ONLINE:
+ raise ValueError("Invalid Server State %s! Must be online" %
self.conn.state)
+
+ # Checking the parameters
+ if not benamebase and not suffix:
+ raise ValueError("Specify either bename or suffix")
+
+ if not input_file:
+ raise ValueError("input_file is mandatory")
+
+ if not os.path.exists(input_file):
+ raise ValueError("Import file (%s) does not exist" % input_file)
+
+ # Prepare the task entry
+ cn = "import_" + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = "cn=%s,%s" % (cn, DN_IMPORT_TASK)
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('nsFilename', input_file)
+ if benamebase:
+ entry.setValues('nsInstance', benamebase)
+ else:
+ entry.setValues('nsIncludeSuffix', suffix)
+
+ # start the task and possibly wait for task completion
+ self.conn.add_s(entry)
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error("Error: import task %s for file %s exited with %d"
%
+ (cn, input_file, exitCode))
+ else:
+ self.log.info("Import task %s for file %s completed successfully"
%
+ (cn, input_file))
+ self.dn = dn
+ self.entry = entry
+ return exitCode
+
+ def exportLDIF(self, suffix=None, benamebase=None, output_file=None,
+ args=None):
+ '''
+ Export in a LDIF format a given 'suffix' (or 'benamebase' that
stores
+ that suffix). It uses an internal task to acheive this request.
+
+ If 'suffix' and 'benamebase' are specified, it uses
'benamebase' first
+ else 'suffix'.
+ If both 'suffix' and 'benamebase' are missing it raises
ValueError
+
+ 'output_file' is the output file of the export
+
+ @param suffix - suffix of the backend
+ @param benamebase - 'commonname'/'cn' of the backend (e.g.
'userRoot')
+ @param output_file - file that will contain the exported suffix in LDIF
+ format
+ @param args - is a dictionary that contains modifier of the export task
+ wait: True/[False] - If True, 'export' waits for the completion
+ of the task before to return
+ repl-info: True/[False] - If True, it adds the replication meta
+ data (state information, tombstones
+ and RUV) in the exported file
+
+ @return None
+
+ @raise ValueError
+
+ '''
+
+ # Checking the parameters
+ if not benamebase and not suffix:
+ raise ValueError("Specify either bename or suffix")
+
+ if not output_file:
+ raise ValueError("output_file is mandatory")
+
+ # Prepare the task entry
+ cn = "export_" + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = "cn=%s,%s" % (cn, DN_EXPORT_TASK)
+ entry = Entry(dn)
+ entry.update({
+ 'objectclass': ['top', 'extensibleObject'],
+ 'cn': cn,
+ 'nsFilename': output_file
+ })
+ if benamebase:
+ entry.setValues('nsInstance', benamebase)
+ else:
+ entry.setValues('nsIncludeSuffix', suffix)
+
+ if args.get(EXPORT_REPL_INFO, False):
+ entry.setValues('nsExportReplica', 'true')
+
+ # start the task and possibly wait for task completion
+ self.conn.add_s(entry)
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error("Error: export task %s for file %s exited with %d"
%
+ (cn, output_file, exitCode))
+ else:
+ self.log.info("Export task %s for file %s completed successfully"
%
+ (cn, output_file))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def db2bak(self, backup_dir=None, args=None):
+ '''
+ Perform a backup by creating a db2bak task
+
+ @param backup_dir - backup directory
+ @param args - is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion of the
+ task before to return
+
+ @return exit code
+
+ @raise ValueError: if bename name does not exist
+ '''
+
+ # Checking the parameters
+ if not backup_dir:
+ raise ValueError("You must specify a backup directory.")
+
+ # build the task entry
+ cn = "backup_" + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = "cn=%s,%s" % (cn, DN_BACKUP_TASK)
+ entry = Entry(dn)
+ entry.update({
+ 'objectclass': ['top', 'extensibleObject'],
+ 'cn': cn,
+ 'nsArchiveDir': backup_dir,
+ 'nsDatabaseType': 'ldbm database'
+ })
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add the backup task (%s)" % dn)
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error("Error: backup task %s exited with %d" %
+ (cn, exitCode))
+ else:
+ self.log.info("Backup task %s completed successfully" % (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def bak2db(self, bename=None, backup_dir=None, args=None):
+ '''
+ Restore a backup by creating a bak2db task
+
+ @param bename - 'commonname'/'cn' of the backend (e.g.
'userRoot')
+ @param backup_dir - backup directory
+ @param args - is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion of
+ the task before to return
+
+ @return exit code
+
+ @raise ValueError: if bename name does not exist
+ '''
+
+ # Checking the parameters
+ if not backup_dir:
+ raise ValueError("You must specify a backup directory")
+ if not os.path.exists(backup_dir):
+ raise ValueError("Backup file (%s) does not exist" % backup_dir)
+
+ # If a backend name was provided then verify it
+ if bename:
+ ents = self.conn.mappingtree.list(bename=bename)
+ if len(ents) != 1:
+ raise ValueError("invalid backend name: %s" % bename)
+
+ # build the task entry
+ cn = "restore_" + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = "cn=%s,%s" % (cn, DN_RESTORE_TASK)
+ entry = Entry(dn)
+ entry.update({
+ 'objectclass': ['top', 'extensibleObject'],
+ 'cn': cn,
+ 'nsArchiveDir': backup_dir,
+ 'nsDatabaseType': 'ldbm database'
+ })
+ if bename:
+ entry.update({'nsInstance': bename})
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add the backup task (%s)" % dn)
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error("Error: restore task %s exited with %d" %
+ (cn, exitCode))
+ else:
+ self.log.info("Restore task %s completed successfully" % (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def reindex(self, suffix=None, benamebase=None, attrname=None, args=None):
+ '''
+ Reindex a 'suffix' (or 'benamebase' that stores that suffix) for
a
+ given 'attrname'. It uses an internal task to acheive this request.
+
+ If 'suffix' and 'benamebase' are specified, it uses
'benamebase' first
+ else 'suffix'.
+ If both 'suffix' and 'benamebase' are missing it raise
ValueError
+
+ @param suffix - suffix of the backend
+ @param benamebase - 'commonname'/'cn' of the backend (e.g.
'userRoot')
+ @param attrname - attribute name
+ @param args - is a dictionary that contains modifier of the reindex
+ task
+ wait: True/[False] - If True, 'index' waits for the completion
+ of the task before to return
+
+ @return None
+
+ @raise ValueError - if invalid missing benamebase and suffix or invalid
+ benamebase
+ @raise LDAPError if unable to search for index names
+
+ '''
+ if not benamebase and not suffix:
+ raise ValueError("Specify either bename or suffix")
+
+ # If backend name was provided, retrieve the suffix
+ if benamebase:
+ ents = self.conn.mappingtree.list(bename=benamebase)
+ if len(ents) != 1:
+ raise ValueError("invalid backend name: %s" % benamebase)
+
+ attr_suffix = MT_PROPNAME_TO_ATTRNAME[MT_SUFFIX]
+ if not ents[0].hasAttr(attr_suffix):
+ raise ValueError(
+ "invalid backend name: %s, or entry without %s" %
+ (benamebase, attr_suffix))
+
+ suffix = ents[0].getValue(attr_suffix)
+
+ entries_backend = self.conn.backend.list(suffix=suffix)
+ backend = entries_backend[0].cn # assume 1 local backend
+ attrs = []
+
+ if not attrname:
+ #
+ # Reindex all attributes - gather them first...
+ #
+ cn = "index_all_%s" % (time.strftime("%m%d%Y_%H%M%S",
+ time.localtime()))
+ dn = ('cn=%s,cn=ldbm database,cn=plugins,cn=config' % backend)
+ try:
+ indexes = self.conn.search_s(dn,
+ ldap.SCOPE_SUBTREE,
+ '(objectclass=nsIndex)')
+ for index in indexes:
+ attrs.append(index.getValue('cn'))
+ except ldap.LDAPError:
+ raise
+ else:
+ #
+ # Reindex specific attribute
+ #
+ cn = "index_%s_%s" % (attrname,
time.strftime("%m%d%Y_%H%M%S",
+ time.localtime()))
+ attrs.append(attrname)
+
+ dn = "cn=%s,%s" % (cn, DN_INDEX_TASK)
+ entry = Entry(dn)
+ entry.update({
+ 'objectclass': ['top', 'extensibleObject'],
+ 'cn': cn,
+ 'nsIndexAttribute': attrs,
+ 'nsInstance': backend
+ })
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add the index task for %s" % attrname)
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error("Error: index task %s exited with %d" %
+ (cn, exitCode))
+ else:
+ self.log.info("Index task %s completed successfully" % (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def fixupMemberOf(self, suffix=None, benamebase=None, filt=None,
+ args=None):
+ '''
+ Trigger a fixup task on 'suffix' (or 'benamebase' that stores
that
+ suffix) related to the entries 'memberof' of groups. It uses an
+ internal task to acheive this request.
+
+ If 'suffix' and 'benamebase' are specified, it uses
'benamebase'
+ first else 'suffix'.
+ If both 'suffix' and 'benamebase' are missing it raise
ValueError
+
+ 'filt' is a filter that will select all the entries (under
+ 'suffix') that we need to evaluate/fix. If missing, the default
+ value is "(|(objectclass=inetuser)(objectclass=inetadmin))"
+
+ @param suffix - suffix of the backend
+ @param benamebase - 'commonname'/'cn' of the backend
+ (e.g. 'userRoot')
+ @param args - is a dictionary that contains modifier of the
+ fixupMemberOf task
+ wait: True/[False] - If True, waits for the completion of the
+ task before to return
+
+ @return exit code
+
+ @raise ValueError: if benamebase and suffix are specified, or can
+ not retrieve the suffix from the mapping tree
+ entry
+ '''
+ if not benamebase and not suffix:
+ raise ValueError("Specify either bename or suffix")
+
+ # If backend name was provided, retrieve the suffix
+ if benamebase:
+ ents = self.conn.mappingtree.list(bename=benamebase)
+ if len(ents) != 1:
+ raise ValueError("invalid backend name: %s" % benamebase)
+
+ attr = MT_PROPNAME_TO_ATTRNAME[MT_SUFFIX]
+ if not ents[0].hasAttr(attr):
+ raise ValueError(
+ "invalid backend name: %s, or entry without %s" %
+ (benamebase, attr))
+
+ suffix = ents[0].getValue(attr)
+
+ cn = "fixupmemberof_" + time.strftime("%m%d%Y_%H%M%S",
+ time.localtime())
+ dn = "cn=%s,%s" % (cn, DN_MBO_TASK)
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('basedn', suffix)
+ if filt:
+ entry.setValues('filter', filt)
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add the memberOf fixup task")
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error(
+ "Error: fixupMemberOf task %s for basedn %s exited with %d" %
+ (cn, suffix, exitCode))
+ else:
+ self.log.info(
+ "fixupMemberOf task %s for basedn %s completed successfully" %
+ (cn, suffix))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def fixupTombstones(self, bename=None, args=None):
+ '''
+ Trigger a tombstone fixup task on the specified backend
+
+ @param bename - 'commonname'/'cn' of the backend (e.g.
'userRoot').
+ Optional.
+ @param args - is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion of the
+ task before to return
+
+ @return exit code
+
+ @raise ValueError: if bename name does not exist
+ '''
+
+ if not bename:
+ bename = DEFAULT_BENAME
+
+ # Verify the backend name
+ if bename:
+ ents = self.conn.mappingtree.list(bename=bename)
+ if len(ents) != 1:
+ raise ValueError("invalid backend name: %s" % bename)
+
+ cn = "fixupTombstone_" + time.strftime("%m%d%Y_%H%M%S",
+ time.localtime())
+ dn = "cn=%s,%s" % (cn, DN_TOMB_FIXUP_TASK)
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('backend', bename)
+ if args and args.get(TASK_TOMB_STRIP, False):
+ entry.setValues('stripcsn', 'yes')
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add the fixup tombstone task")
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error(
+ "Error: tombstone fixup task %s for backend %s exited with %d"
+ % (cn, bename, exitCode))
+ else:
+ self.log.info(
+ "tombstone fixup task %s for backend %s completed
successfully"
+ % (cn, bename))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def automemberRebuild(self, suffix=DEFAULT_SUFFIX, scope='sub',
+ filterstr='objectclass=top', args=None):
+ '''
+ @param suffix - The suffix the task should examine - defualt is
+ "dc=example,dc=com"
+ @param scope - The scope of the search to find entries
+ @param fitlerstr - THe search filter to find entries
+ @param args - is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion of
+ the task before to return
+ @return exit code
+ '''
+
+ cn = 'task-' + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = ('cn=%s,cn=automember rebuild membership,cn=tasks,cn=config' % cn)
+
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('basedn', suffix)
+ entry.setValues('filter', filterstr)
+ entry.setValues('scope', scope)
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add Automember Rebuild Membership task")
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error(
+ "Error: Automember Rebuild Membership task (%s) exited with
%d"
+ % (cn, exitCode))
+ else:
+ self.log.info(
+ "Automember Rebuild Membership task(%s) completed
successfully"
+ % (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def automemberExport(self, suffix=DEFAULT_SUFFIX, scope='sub',
+ fstr='objectclass=top', ldif_out=None, args=None):
+ '''
+ @param suffix - The suffix the task should examine - default is
+ "dc=example,dc=com"
+ @param scope - The scope of the search to find entries
+ @param fstr - The search filter to find entries
+ @param ldif_out - The name for the output LDIF file
+ @param args - is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion of
+ the task before to return
+ @return exit code
+ @raise ValueError: if ldif_out is not provided
+ '''
+
+ if not ldif_out:
+ raise ValueError("Missing ldif_out")
+
+ cn = 'task-' + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = ('cn=%s,cn=automember export updates,cn=tasks,cn=config' % cn)
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('basedn', suffix)
+ entry.setValues('filter', fstr)
+ entry.setValues('scope', scope)
+ entry.setValues('ldif', ldif_out)
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add Automember Export Updates task")
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error(
+ "Error: Automember Export Updates task (%s) exited with %d" %
+ (cn, exitCode))
+ else:
+ self.log.info(
+ "Automember Export Updates task (%s) completed successfully" %
+ (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def automemberMap(self, ldif_in=None, ldif_out=None, args=None):
+ '''
+ @param ldif_in - Entries to pass into the task for processing
+ @param ldif_out - The resulting LDIF of changes from ldif_in
+ @param args - is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion
+ of the task before to return
+ @return exit code
+ @raise ValueError: if ldif_out/ldif_in is not provided
+ '''
+
+ if not ldif_out or not ldif_in:
+ raise ValueError("Missing ldif_out and/or ldif_in")
+
+ cn = 'task-' + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = ('cn=%s,cn=automember map updates,cn=tasks,cn=config' % cn)
+
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('ldif_in', ldif_in)
+ entry.setValues('ldif_out', ldif_out)
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add Automember Map Updates task")
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error(
+ "Error: Automember Map Updates task (%s) exited with %d" %
+ (cn, exitCode))
+ else:
+ self.log.info(
+ "Automember Map Updates task (%s) completed successfully" %
+ (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def fixupLinkedAttrs(self, linkdn=None, args=None):
+ '''
+ @param linkdn - The DN of linked attr config entry (if None all
+ possible configurations are checked)
+ @param args - Is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion
+ of the task before to return
+ @return exit code
+ '''
+
+ cn = 'task-' + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = ('cn=%s,cn=fixup linked attributes,cn=tasks,cn=config' % cn)
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ if linkdn:
+ entry.setValues('linkdn', linkdn)
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add Fixup Linked Attributes task")
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error(
+ "Error: Fixup Linked Attributes task (%s) exited with %d" %
+ (cn, exitCode))
+ else:
+ self.log.info(
+ "Fixup Linked Attributes task (%s) completed successfully" %
+ (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def schemaReload(self, schemadir=None, args=None):
+ '''
+ @param schemadir - The directory to look for schema files(optional)
+ @param args - Is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion of
+ the task before to return
+ @return exit code
+ '''
+
+ cn = 'task-' + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = ('cn=%s,cn=schema reload task,cn=tasks,cn=config' % cn)
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ if schemadir:
+ entry.setValues('schemadir', schemadir)
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add Schema Reload task")
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error("Error: Schema Reload task (%s) exited with %d" %
+ (cn, exitCode))
+ else:
+ self.log.info("Schema Reload task (%s) completed successfully" %
+ (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def fixupWinsyncMembers(self, suffix=DEFAULT_SUFFIX,
+ fstr='objectclass=top', args=None):
+ '''
+ @param suffix - The suffix the task should rebuild -
+ default is "dc=example,dc=com"
+ @param fstr - The search filter to find entries
+ @param args - is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion of
+ the task before to return
+ @return exit code
+ '''
+
+ cn = 'task-' + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = ('cn=%s,cn=memberuid task,cn=tasks,cn=config' % cn)
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('basedn', suffix)
+ entry.setValues('filter', fstr)
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add fixupWinsyncMembers 'memberuid
task'")
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error(
+ "fixupWinsyncMembers 'memberuid task' (%s) exited with
%d" %
+ (cn, exitCode))
+ else:
+ self.log.info(
+ "'memberuid task' (%s) completed successfully" % (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def syntaxValidate(self, suffix=DEFAULT_SUFFIX, fstr='objectclass=top',
+ args=None):
+ '''
+ @param suffix - The suffix the task should validate -
+ default is "dc=example,dc=com"
+ @param fstr - The search filter to find entries
+ @param args - is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion of the
+ task before to return
+ @return exit code
+ '''
+
+ cn = 'task-' + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = ('cn=%s,cn=syntax validate,cn=tasks,cn=config' % cn)
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('basedn', suffix)
+ entry.setValues('filter', fstr)
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add Syntax Validate task")
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error("Error: Syntax Validate (%s) exited with %d" %
+ (cn, exitCode))
+ else:
+ self.log.info("Syntax Validate task (%s) completed successfully" %
+ (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def usnTombstoneCleanup(self, suffix=DEFAULT_SUFFIX, bename=None,
+ maxusn_to_delete=None, args=None):
+ '''
+ @param suffix - The suffix the task should cleanup - default is
+ "dc=example,dc=com"
+ @param backend - The 'backend' the task should cleanup
+ @param maxusn_to_delete - Maximum number of usn's to delete
+ @param args - is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion of
+ the task before to return
+ @return exit code
+ '''
+
+ cn = 'task-' + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = ('cn=%s,cn=USN tombstone cleanup task,cn=tasks,cn=config' % cn)
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ if not bename:
+ entry.setValues('suffix', suffix)
+ else:
+ entry.setValues('backend', bename)
+ if maxusn_to_delete:
+ entry.setValues('maxusn_to_delete')
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add USN tombstone cleanup task")
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error(
+ "Error: USN tombstone cleanup task (%s) exited with %d" %
+ (cn, exitCode))
+ else:
+ self.log.info(
+ "USN tombstone cleanup task (%s) completed successfully" %
+ (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def sysconfigReload(self, configfile=None, logchanges=None, args=None):
+ '''
+ @param configfile - The sysconfig file:
+ /etc/sysconfig/dirsrv-localhost
+ @param logchanges - True/False - Tell the server to log the changes
+ made by the task
+ @param args - is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion of the
+ task before to return
+ @return exit code
+ @raise ValueError: If sysconfig file not provided
+ '''
+
+ if not configfile:
+ raise ValueError("Missing required paramter: configfile")
+
+ cn = 'task-' + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = ('cn=%s,cn=cn=sysconfig reload,cn=tasks,cn=config' % cn)
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('sysconfigfile', configfile)
+ if logchanges:
+ entry.setValues('logchanges', logchanges)
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add Sysconfig Reload task")
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error("Error: Sysconfig Reload task (%s) exited with %d"
%
+ (cn, exitCode))
+ else:
+ self.log.info("Sysconfig Reload task (%s) completed successfully"
%
+ (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
+
+ def cleanAllRUV(self, suffix=None, replicaid=None, force=None, args=None):
+ '''
+ @param replicaid - The replica ID to remove/clean
+ @param force - True/False - Clean all the replicas, even if one is down
+ @param args - is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion of the
+ task before to return
+ @return tuple (task dn, and the exit code)
+ @raise ValueError: If missing replicaid
+ '''
+
+ if not replicaid:
+ raise ValueError("Missing required paramter: replicaid")
+
+ if not suffix:
+ raise ValueError("Missing required paramter: suffix")
+
+ cn = 'task-' + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = ('cn=%s,cn=cleanallruv,cn=tasks,cn=config' % cn)
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('replica-base-dn', suffix)
+ entry.setValues('replica-id', replicaid)
+ if force:
+ entry.setValues('replica-force-cleaning', 'yes')
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add cleanAllRUV task")
+ return (dn, -1)
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error("Error: cleanAllRUV task (%s) exited with %d" %
+ (cn, exitCode))
+ else:
+ self.log.info("cleanAllRUV task (%s) completed successfully" %
+ (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return (dn, exitCode)
+
+ def abortCleanAllRUV(self, suffix=None, replicaid=None, certify=None,
+ args=None):
+ '''
+ @param replicaid - The replica ID to remove/clean
+ @param certify - True/False - Certify the task was aborted on all
+ the replicas
+ @param args - is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion of
+ the task before to return
+ @return tuple (task dn, and the exit code)
+ @raise ValueError: If missing replicaid
+ '''
+
+ if not replicaid:
+ raise ValueError("Missing required paramter: replicaid")
+ if not suffix:
+ raise ValueError("Missing required paramter: suffix")
+
+ cn = 'task-' + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = ('cn=%s,cn=abort cleanallruv,cn=tasks,cn=config' % cn)
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('replica-base-dn', suffix)
+ entry.setValues('replica-id', replicaid)
+ if certify:
+ entry.setValues('replica-certify-all', 'yes')
+ else:
+ entry.setValues('replica-certify-all', 'no')
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add Abort cleanAllRUV task")
+ return (dn, -1)
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error(
+ "Error: Abort cleanAllRUV task (%s) exited with %d" %
+ (cn, exitCode))
+ else:
+ self.log.info(
+ "Abort cleanAllRUV task (%s) completed successfully" %
+ (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return (dn, exitCode)
+
+ def upgradeDB(self, nsArchiveDir=None, nsDatabaseType=None,
+ nsForceToReindex=None, args=None):
+ '''
+ @param nsArchiveDir - The archive directory
+ @param nsDatabaseType - The database type - default is "ldbm database"
+ @param nsForceToReindex - True/False - force reindexing to occur
+ @param args - is a dictionary that contains modifier of the task
+ wait: True/[False] - If True, waits for the completion of the
+ task before to return
+ @return exit code
+ @raise ValueError: If missing nsArchiveDir
+ '''
+
+ if not nsArchiveDir:
+ raise ValueError("Missing required paramter: nsArchiveDir")
+
+ cn = 'task-' + time.strftime("%m%d%Y_%H%M%S",
time.localtime())
+ dn = ('cn=%s,cn=upgradedb,cn=tasks,cn=config' % cn)
+ entry = Entry(dn)
+ entry.setValues('objectclass', 'top',
'extensibleObject')
+ entry.setValues('cn', cn)
+ entry.setValues('nsArchiveDir', nsArchiveDir)
+ if nsDatabaseType:
+ entry.setValues('nsDatabaseType', nsDatabaseType)
+ if nsForceToReindex:
+ entry.setValues('nsForceToReindex', 'True')
+
+ # start the task and possibly wait for task completion
+ try:
+ self.conn.add_s(entry)
+ except ldap.ALREADY_EXISTS:
+ self.log.error("Fail to add upgradedb task")
+ return -1
+
+ exitCode = 0
+ if args and args.get(TASK_WAIT, False):
+ (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+
+ if exitCode:
+ self.log.error("Error: upgradedb task (%s) exited with %d" %
+ (cn, exitCode))
+ else:
+ self.log.info("Upgradedb task (%s) completed successfully" % (cn))
+
+ self.dn = dn
+ self.entry = entry
+
+ return exitCode
diff --git a/src/lib389/lib389/tests/__init__.py b/src/lib389/lib389/tests/__init__.py
new file mode 100644
index 0000000..f5bbefe
--- /dev/null
+++ b/src/lib389/lib389/tests/__init__.py
@@ -0,0 +1,7 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
diff --git a/src/lib389/lib389/tests/aci_test.py b/src/lib389/lib389/tests/aci_test.py
new file mode 100644
index 0000000..05d88f7
--- /dev/null
+++ b/src/lib389/lib389/tests/aci_test.py
@@ -0,0 +1,122 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+from lib389._constants import *
+from lib389 import DirSrv, Entry
+import pytest
+from lib389.utils import ensure_bytes, ensure_str
+
+import sys
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+# INSTANCE_PREFIX = None
+
+
+class TopologyStandalone(object):
+ def __init__(self, standalone):
+ standalone.open()
+ self.standalone = standalone
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ standalone = DirSrv(verbose=False)
+ standalone.log.debug("Instance allocated")
+ args = {SER_HOST: LOCALHOST,
+ SER_PORT: INSTANCE_PORT,
+ # SER_DEPLOYED_DIR: INSTANCE_PREFIX,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ standalone.allocate(args)
+ if standalone.exists():
+ standalone.delete()
+ standalone.create()
+ standalone.open()
+
+ def fin():
+ standalone.delete()
+ request.addfinalizer(fin)
+
+ return TopologyStandalone(standalone)
+
+
+(a)pytest.fixture(scope="module")
+def complex_aci(topology):
+ ACI_TARGET = ('(targetfilter ="(ou=groups)")(targetattr
="uniqueMember '
+ '|| member")')
+ ACI_ALLOW = ('(version 3.0; acl "Allow test aci";allow (read, search,
'
+ 'write)')
+ ACI_SUBJECT =
('(userdn="ldap:///dc=example,dc=com??sub?(ou=engineering)" '
+ 'and
userdn="ldap:///dc=example,dc=com??sub?(manager=uid='
+ 'wbrown,ou=managers,dc=example,dc=com) || ldap:///dc=examp'
+ 'le,dc=com??sub?(manager=uid=tbrown,ou=managers,dc=exampl'
+ 'e,dc=com)" );)')
+ ACI_BODY = ACI_TARGET + ACI_ALLOW + ACI_SUBJECT
+
+ gentry = Entry('cn=testgroup,%s' % DEFAULT_SUFFIX)
+ gentry.setValues('objectclass', 'top', 'extensibleobject')
+ gentry.setValues('cn', 'testgroup')
+ gentry.setValues('aci', ACI_BODY)
+
+ topology.standalone.add_s(gentry)
+
+ return ensure_str(ACI_BODY)
+
+
+def test_aci(topology, complex_aci):
+ """Checks content of previously added aci"""
+
+ acis = topology.standalone.aci.list('cn=testgroup,%s' % DEFAULT_SUFFIX)
+ assert len(acis) == 1
+ aci = acis[0]
+ print(aci.acidata)
+ assert aci.acidata == {
+ 'allow': [{'values': ['read', 'search',
'write']}],
+ 'target': [], 'targetattr': [{'values':
['uniqueMember', 'member'],
+ 'equal': True}],
+ 'targattrfilters': [],
+ 'deny': [],
+ 'acl': [{'values': ['Allow test aci']}],
+ 'deny_raw_bindrules': [],
+ 'targetattrfilters': [],
+ 'allow_raw_bindrules': [{'values': [(
+ 'userdn="ldap:///dc=example,dc=com??sub?(ou=engineering)"
and'
+ ' userdn="ldap:///dc=example,dc=com??sub?(manager=uid=wbrown,'
+ 'ou=managers,dc=example,dc=com) || ldap:///dc=example,dc=com'
+ '??sub?(manager=uid=tbrown,ou=managers,dc=example,dc=com)"
')]}],
+ 'targetfilter': [{'values': ['(ou=groups)'],
'equal': True}],
+ 'targetscope': [],
+ 'version 3.0;': [],
+ 'rawaci': complex_aci
+ }
+ assert aci.getRawAci() == complex_aci
+
+
+def test_aci_lint(topology, complex_aci):
+ """Checks content of previously added aci"""
+
+ # Make sure we actually have acis to check!
+ acis = topology.standalone.aci.list('cn=testgroup,%s' % DEFAULT_SUFFIX)
+ # print(acis)
+ assert len(acis) == 1
+
+ # Actually test them
+ (result, detail) = topology.standalone.aci.lint(DEFAULT_SUFFIX)
+ # What is the point of testing perfect acis! We need this to fail!
+ assert (result is False)
+ # print(topology.standalone.aci.format_lint(detail))
+
+ assert(len(detail) == 2)
+ assert (detail[0]['dsale'] == 'DSALE0001')
+ assert (detail[1]['dsale'] == 'DSALE0002')
+ # Check all the results
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -vv %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/agreement_test.py
b/src/lib389/lib389/tests/agreement_test.py
new file mode 100644
index 0000000..c39b08c
--- /dev/null
+++ b/src/lib389/lib389/tests/agreement_test.py
@@ -0,0 +1,362 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import ldap
+import time
+import os
+import pytest
+
+from lib389 import NoSuchEntryError
+from lib389.agreement import Agreement
+from lib389._constants import *
+from lib389.properties import *
+from lib389 import DirSrv, Entry
+from lib389.utils import ensure_bytes, ensure_str
+from lib389.idm.user import UserAccounts, TEST_USER_PROPERTIES
+from lib389.idm.domain import Domain
+
+# Used for One master / One consumer topology
+HOST_MASTER = LOCALHOST
+PORT_MASTER = 40389
+SERVERID_MASTER = 'master'
+REPLICAID_MASTER = 1
+
+HOST_CONSUMER = LOCALHOST
+PORT_CONSUMER = 50389
+SERVERID_CONSUMER = 'consumer'
+
+SUFFIX = DEFAULT_SUFFIX
+ENTRY_DN = "cn=test_entry, %s" % SUFFIX
+SECOND_AGMT_TEST_PORT = 12345
+
+
+class TopologyReplication(object):
+ def __init__(self, master, consumer):
+ master.open()
+ consumer.open()
+ self.master = master
+ self.consumer = consumer
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ # Master
+ #
+ # Create the master instance
+ master = DirSrv(verbose=False)
+ master.log.debug("Master allocated")
+ args = {SER_HOST: HOST_MASTER,
+ SER_PORT: PORT_MASTER,
+ SER_SERVERID_PROP: SERVERID_MASTER}
+ master.allocate(args)
+ if master.exists():
+ master.delete()
+ master.create()
+ master.open()
+
+ # Enable replication
+ master.replica.enableReplication(suffix=SUFFIX,
+ role=ReplicaRole.MASTER,
+ replicaId=REPLICAID_MASTER)
+
+ # Consumer
+ #
+ # Create the consumer instance
+ consumer = DirSrv(verbose=False)
+ consumer.log.debug("Consumer allocated")
+ args = {SER_HOST: HOST_CONSUMER,
+ SER_PORT: PORT_CONSUMER,
+ SER_SERVERID_PROP: SERVERID_CONSUMER}
+ consumer.allocate(args)
+ if consumer.exists():
+ consumer.delete()
+ consumer.create()
+ consumer.open()
+
+ # Enable replication
+ consumer.replica.enableReplication(suffix=SUFFIX,
+ role=ReplicaRole.CONSUMER)
+
+ # Delete each instance in the end
+ def fin():
+ master.delete()
+ consumer.delete()
+ request.addfinalizer(fin)
+
+ return TopologyReplication(master, consumer)
+
+
+def test_create(topology):
+ """Test to create a replica agreement and initialize the consumer.
+ Test on a unknown suffix
+ """
+
+ topology.master.log.info("\n\n##############\n##
CREATE\n##############\n")
+ properties = {RA_NAME: ('meTo_%s:%d' % (topology.consumer.host,
+ topology.consumer.port)),
+ RA_BINDDN: defaultProperties[REPLICATION_BIND_DN],
+ RA_BINDPW: defaultProperties[REPLICATION_BIND_PW],
+ RA_METHOD: defaultProperties[REPLICATION_BIND_METHOD],
+ RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
+ repl_agreement = topology.master.agreement.create(
+ suffix=SUFFIX, host=topology.consumer.host,
+ port=topology.consumer.port, properties=properties)
+ topology.master.log.debug("%s created" % repl_agreement)
+ topology.master.agreement.init(SUFFIX, HOST_CONSUMER, PORT_CONSUMER)
+ topology.master.waitForReplInit(repl_agreement)
+
+ master_users = UserAccounts(topology.master, SUFFIX)
+ consumer_users = UserAccounts(topology.consumer, SUFFIX)
+
+ testuser = master_users.create(properties=TEST_USER_PROPERTIES)
+ testuser_dn = testuser.dn
+
+ # Add a test entry
+ # Check replication is working
+ loop = 0
+ while loop <= 10:
+ try:
+ consumer_users.get(dn=testuser_dn)
+ break
+ except ldap.NO_SUCH_OBJECT:
+ time.sleep(1)
+ loop += 1
+ assert loop <= 10
+
+ # Check that with an invalid suffix it raises NoSuchEntryError
+ with pytest.raises(NoSuchEntryError):
+ properties = {RA_NAME: r'meAgainTo_%s:%d' %
+ (topology.consumer.host, topology.consumer.port)}
+ topology.master.agreement.create(suffix="ou=dummy",
+ host=topology.consumer.host,
+ port=topology.consumer.port,
+ properties=properties)
+
+
+def test_list(topology):
+ """List the replica agreement on a suffix => 1
+ Add a RA
+ List the replica agreements on that suffix again => 2
+ List a specific RA
+
+ PREREQUISITE: it exists a replica for SUFFIX and a replica agreement
+ """
+
+ topology.master.log.info("\n\n###########\n## LIST\n#############\n")
+ ents = topology.master.agreement.list(suffix=SUFFIX)
+ assert len(ents) == 1
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_HOST]) == \
+ ensure_bytes(topology.consumer.host)
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_PORT]) == \
+ ensure_bytes(str(topology.consumer.port))
+
+ # Create a second RA to check .list returns 2 RA
+ properties = {RA_NAME: r'meTo_%s:%d' % (topology.consumer.host,
+ SECOND_AGMT_TEST_PORT),
+ RA_BINDDN: defaultProperties[REPLICATION_BIND_DN],
+ RA_BINDPW: defaultProperties[REPLICATION_BIND_PW],
+ RA_METHOD: defaultProperties[REPLICATION_BIND_METHOD],
+ RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
+ topology.master.agreement.create(suffix=SUFFIX,
+ host=topology.consumer.host,
+ port=SECOND_AGMT_TEST_PORT,
+ properties=properties)
+ ents = topology.master.agreement.list(suffix=SUFFIX)
+ assert len(ents) == 2
+
+ # Check we can .list a specific RA
+ ents = topology.master.agreement.list(suffix=SUFFIX,
+ consumer_host=topology.consumer.host,
+ consumer_port=topology.consumer.port)
+ assert len(ents) == 1
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_HOST]) == \
+ ensure_bytes(topology.consumer.host)
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_CONSUMER_PORT]) == \
+ ensure_bytes(str(topology.consumer.port))
+
+
+def test_delete(topology):
+ """Delete previously created the replica agreement
+
+ PREREQUISITE: it exists a replica for SUFFIX and a replica agreement
+ with a SECOND_AGMT_TEST_PORT specified
+ """
+
+ # TODO: add a few test cases for different sets of arguments
+ # using fixtures and parameters.
+ topology.master.log.info("\n\n##############\n###
DELETE\n##############\n")
+ ents = topology.master.agreement.list(suffix=SUFFIX,
+ consumer_host=topology.consumer.host,
+ consumer_port=SECOND_AGMT_TEST_PORT)
+ assert len(ents) == 1
+
+ # Find DN of the second agreement
+ replica_entries = topology.master.replica.list(SUFFIX)
+ replica = replica_entries[0]
+ agreement_cn = r'meTo_%s:%d' % (topology.consumer.host,
+ SECOND_AGMT_TEST_PORT)
+ agreement_dn = ','.join(["cn=%s" % agreement_cn, replica.dn])
+
+ topology.master.agreement.delete(suffix=SUFFIX,
+ consumer_host=topology.consumer.host,
+ consumer_port=SECOND_AGMT_TEST_PORT,
+ agmtdn=agreement_dn)
+
+ ents = topology.master.agreement.list(suffix=SUFFIX,
+ consumer_host=topology.consumer.host,
+ consumer_port=SECOND_AGMT_TEST_PORT)
+ assert not ents
+
+
+def test_status(topology):
+ """Test that status is returned from agreement"""
+
+ topology.master.log.info("\n\n###########\n## STATUS\n##########")
+ ents = topology.master.agreement.list(suffix=SUFFIX)
+ for ent in ents:
+ ra_status = topology.master.agreement.status(ensure_str(ent.dn))
+ assert ra_status
+ topology.master.log.info("Status of %s: %s" % (ent.dn, ra_status))
+
+
+def test_schedule(topology):
+ """Test the schedule behaviour with valid and invalid
values"""
+
+ topology.master.log.info("\n\n###########\n## SCHEDULE\n#########")
+ ents = topology.master.agreement.list(suffix=SUFFIX,
+ consumer_host=topology.consumer.host,
+ consumer_port=topology.consumer.port)
+ assert len(ents) == 1
+
+ topology.master.agreement.schedule(ents[0].dn, Agreement.ALWAYS)
+ ents = topology.master.agreement.list(suffix=SUFFIX,
+ consumer_host=topology.consumer.host,
+ consumer_port=topology.consumer.port)
+ assert len(ents) == 1
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_SCHEDULE]) == \
+ ensure_bytes(Agreement.ALWAYS)
+
+ topology.master.agreement.schedule(ents[0].dn, Agreement.NEVER)
+ ents = topology.master.agreement.list(suffix=SUFFIX,
+ consumer_host=topology.consumer.host,
+ consumer_port=topology.consumer.port)
+ assert len(ents) == 1
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_SCHEDULE]) == \
+ ensure_bytes(Agreement.NEVER)
+
+ CUSTOM_SCHEDULE = "0000-1234 6420"
+ topology.master.agreement.schedule(ents[0].dn, CUSTOM_SCHEDULE)
+ ents = topology.master.agreement.list(suffix=SUFFIX,
+ consumer_host=topology.consumer.host,
+ consumer_port=topology.consumer.port)
+ assert len(ents) == 1
+ assert ents[0].getValue(RA_PROPNAME_TO_ATTRNAME[RA_SCHEDULE]) == \
+ ensure_bytes(CUSTOM_SCHEDULE)
+
+ CUSTOM_SCHEDULES = ("2500-1234 6420", # Invalid HOUR schedule
+ "0000-2534 6420", # ^^
+ "1300-1234 6420", # Starting HOUR after ending HOUR
+ "0062-1234 6420", # Invalid MIN schedule
+ "0000-1362 6420", # ^^
+ "0000-1234 6-420", # Invalid DAYS schedule
+ "0000-1362 64209", # ^^
+ "0000-1362 01234560") # ^^
+
+ for CUSTOM_SCHEDULE in CUSTOM_SCHEDULES:
+ with pytest.raises(ValueError):
+ topology.master.agreement.schedule(ents[0].dn, CUSTOM_SCHEDULE)
+
+
+def test_getProperties(topology):
+ """Check the correct behaviour of getProperties
function"""
+
+ topology.master.log.info("\n\n###########\n##
GETPROPERTIES\n############")
+ ents = topology.master.agreement.list(suffix=SUFFIX,
+ consumer_host=topology.consumer.host,
+ consumer_port=topology.consumer.port)
+ assert len(ents) == 1
+ properties = topology.master.agreement.getProperties(agmnt_dn=ents[0].dn)
+ for prop in properties:
+ topology.master.log.info("RA %s : %s -> %s" %
+ (prop, RA_PROPNAME_TO_ATTRNAME[prop],
+ properties[prop]))
+
+ properties = topology.master.agreement.getProperties(
+ agmnt_dn=ents[0].dn, properties=[RA_BINDDN])
+ assert len(properties) == 1
+ for prop in properties:
+ topology.master.log.info("RA %s : %s -> %s" %
+ (prop, RA_PROPNAME_TO_ATTRNAME[prop],
+ properties[prop]))
+
+
+def test_setProperties(topology):
+ """Set properties to the agreement and check, if it was
successful"""
+
+ topology.master.log.info("\n\n###########\n### SETPROPERTIES\n##########")
+ ents = topology.master.agreement.list(suffix=SUFFIX,
+ consumer_host=topology.consumer.host,
+ consumer_port=topology.consumer.port)
+ assert len(ents) == 1
+ test_schedule = "1234-2345 12345"
+ test_desc = "test_desc"
+ topology.master.agreement.setProperties(
+ agmnt_dn=ents[0].dn, properties={RA_SCHEDULE: test_schedule,
+ RA_DESCRIPTION: test_desc})
+ properties = topology.master.agreement.getProperties(
+ agmnt_dn=ents[0].dn, properties=[RA_SCHEDULE, RA_DESCRIPTION])
+ assert len(properties) == 2
+ assert properties[RA_SCHEDULE][0] == ensure_bytes(test_schedule)
+ assert properties[RA_DESCRIPTION][0] == ensure_bytes(test_desc)
+
+ # Set RA Schedule back to "always"
+ topology.master.agreement.schedule(ents[0].dn, Agreement.ALWAYS)
+
+
+def test_changes(topology):
+ """Test the changes counter behaviour after making some changes
+ to the replicated suffix
+ """
+
+ topology.master.log.info("\n\n##########\n### CHANGES\n##########")
+ ents = topology.master.agreement.list(suffix=SUFFIX,
+ consumer_host=topology.consumer.host,
+ consumer_port=topology.consumer.port)
+ assert len(ents) == 1
+ value = topology.master.agreement.changes(agmnt_dn=ents[0].dn)
+ topology.master.log.info("\ntest_changes: %d changes\n" % value)
+ assert value > 0
+
+ # Do an update
+ TEST_STRING = 'test_string'
+ master_domain = Domain(topology.master, SUFFIX)
+ consumer_domain = Domain(topology.consumer, SUFFIX)
+
+ master_domain.set('description', TEST_STRING)
+
+ # The update has been replicated
+ loop = 0
+ while loop <= 10:
+ if consumer_domain.present('description', TEST_STRING):
+ break
+ time.sleep(1)
+ loop += 1
+ assert loop <= 10
+
+ # Give a little time to update a change number on master
+ time.sleep(2)
+
+ # Check change number
+ newvalue = topology.master.agreement.changes(agmnt_dn=ents[0].dn)
+ topology.master.log.info("\ntest_changes: %d changes\n" % newvalue)
+ assert (value + 1) == newvalue
+
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/backend_test.py
b/src/lib389/lib389/tests/backend_test.py
new file mode 100644
index 0000000..e000850
--- /dev/null
+++ b/src/lib389/lib389/tests/backend_test.py
@@ -0,0 +1,282 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import os
+import pytest
+import logging
+import ldap
+
+from lib389._constants import *
+from lib389.properties import *
+from lib389 import DirSrv, InvalidArgumentError, UnwillingToPerformError
+
+logging.getLogger(__name__).setLevel(logging.DEBUG)
+log = logging.getLogger(__name__)
+
+TEST_REPL_DN = "uid=test,%s" % DEFAULT_SUFFIX
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+INSTANCE_PREFIX = None
+INSTANCE_BACKUP = os.environ.get('BACKUPDIR', DEFAULT_BACKUPDIR)
+NEW_SUFFIX_1 = 'o=test_create'
+NEW_BACKEND_1 = 'test_createdb'
+NEW_CHILDSUFFIX_1 = 'o=child1,o=test_create'
+NEW_CHILDBACKEND_1 = 'test_createchilddb'
+NEW_SUFFIX_2 = 'o=test_bis_create'
+NEW_BACKEND_2 = 'test_bis_createdb'
+NEW_CHILDSUFFIX_2 = 'o=child2,o=test_bis_create'
+NEW_CHILDBACKEND_2 = 'test_bis_createchilddb'
+
+DEBUGGING = False
+
+
+class TopologyStandalone(object):
+ def __init__(self, standalone):
+ standalone.open()
+ self.standalone = standalone
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ standalone = DirSrv(verbose=DEBUGGING)
+ standalone.log.debug("Instance allocated")
+ args = {SER_HOST: LOCALHOST,
+ SER_PORT: INSTANCE_PORT,
+ SER_DEPLOYED_DIR: INSTANCE_PREFIX,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ standalone.allocate(args)
+ if standalone.exists():
+ standalone.delete()
+ standalone.create()
+ standalone.open()
+
+ def fin():
+ if not DEBUGGING:
+ standalone.delete()
+ request.addfinalizer(fin)
+
+ return TopologyStandalone(standalone)
+
+
+def test_list(topology):
+ """Test backend.list() function behaviour after:
+ - creating new suffixes
+ - filter with suffix
+ - filter with backend name (bename)
+ - filter with backend dn
+ - filter with invalid suffix/bename
+ """
+
+ ents = topology.standalone.backends.list()
+ nb_backend = len(ents)
+ for ent in ents:
+ topology.standalone.log.info("List(%d): backend %s" %
+ (nb_backend, ent.dn))
+
+ log.info("Create a first backend and check list all backends")
+ topology.standalone.backends.create(
+ None, properties={
+ BACKEND_NAME: NEW_BACKEND_1,
+ 'suffix': NEW_SUFFIX_1,
+ })
+
+ ents = topology.standalone.backends.list()
+ for ent in ents:
+ topology.standalone.log.info("List(%d): backend %s" %
+ (nb_backend + 1, ent.dn))
+ assert len(ents) == (nb_backend + 1)
+
+ log.info("Create a second backend and check list all backends")
+ topology.standalone.backends.create(
+ None, properties={
+ BACKEND_NAME: NEW_BACKEND_2,
+ 'suffix': NEW_SUFFIX_2,
+ })
+
+ ents = topology.standalone.backends.list()
+ for ent in ents:
+ topology.standalone.log.info("List(%d): backend %s" %
+ (nb_backend + 2, ent.dn))
+ assert len(ents) == (nb_backend + 2)
+
+ log.info("Check list a backend per suffix")
+ # In the new api this becomes a get over the selector type
+ # In the case this fails, this actually throws exception
+ ent = topology.standalone.backends.get(NEW_SUFFIX_1)
+ topology.standalone.log.info("List suffix (%d): backend %s" %
+ (1, ent.dn))
+ assert ent is not None
+
+ log.info("Check list a backend by its name")
+ ent = topology.standalone.backends.get(NEW_BACKEND_2)
+ topology.standalone.log.info("List name (%d): backend %s" %
+ (1, ent.dn))
+ assert ent is not None
+
+ log.info("Check get backends by their DN")
+ ents = topology.standalone.backends.get(dn=ent.dn)
+ topology.standalone.log.info("List DN (%d): backend %s" % (1, ents.dn))
+ assert ents is not None
+
+ # The new backends api just does selection on a single attr, and
+ # You would then validate the other attributes on retrival.
+ # But what would really be a case for this? If you know the suffix, get
+ # that. If you know the name, get that. Why both?
+ # log.info("Check list with valid backend DN but invalid suffix/bename")
+ # all = topology.standalone.backend.list()
+ # for ent in all:
+ # ents = topology.standalone.backend.list(suffix="o=dummy",
+ # backend_dn=ent.dn,
+ # bename="dummydb")
+ # for bck in ents:
+ # topology.standalone.log.info("List invalid suffix+bename "
+ # "(%d): backend %s" % (1, bck.dn))
+ # assert len(ents) == 1
+
+ log.info("Just to make it clean in the end")
+ b1 = topology.standalone.backends.get(NEW_SUFFIX_1)
+ b2 = topology.standalone.backends.get(NEW_SUFFIX_2)
+ b1.delete()
+ b2.delete()
+
+
+def test_create(topology):
+ """Test backend.create() function with:
+ - specifying no suffix
+ - specifying already existing backend suffix
+ - specifying already existing backend name, but new suffix
+ - specifying no properties
+ """
+
+ log.info("Create a backend")
+ topology.standalone.backend.create(suffix=NEW_SUFFIX_1,
+ properties={BACKEND_NAME:
+ NEW_BACKEND_1})
+
+ log.info("Check behaviour with missing suffix")
+ with pytest.raises(ldap.UNWILLING_TO_PERFORM) as excinfo:
+ topology.standalone.backend.create()
+ assert 'Missing Suffix' in str(excinfo.value)
+
+ log.info("Check behaviour with already existing backend for that suffix")
+ with pytest.raises(ldap.ALREADY_EXISTS):
+ topology.standalone.backend.create(suffix=NEW_SUFFIX_1)
+
+ log.info("Check behaviour with already existing backend DN, "
+ "but new suffix")
+ with pytest.raises(ldap.ALREADY_EXISTS) as excinfo:
+ topology.standalone.backend.create(suffix=NEW_SUFFIX_2,
+ properties={BACKEND_NAME:
+ NEW_BACKEND_1})
+ assert 'Backend already exists' in str(excinfo.value)
+
+ log.info("Create a backend without properties")
+ topology.standalone.backend.create(suffix=NEW_SUFFIX_2)
+ ents = topology.standalone.backend.list(suffix=NEW_SUFFIX_2)
+ assert len(ents) == 1
+
+ log.info("Just to make it clean in the end")
+ topology.standalone.backend.delete(suffix=NEW_SUFFIX_1)
+ topology.standalone.backend.delete(suffix=NEW_SUFFIX_2)
+
+
+def test_delete_valid(topology):
+ """Test the various possibilities to delete a backend:
+ - with suffix
+ - with backend name
+ - with backend DN
+ """
+
+ ents = topology.standalone.backend.list()
+ nb_backend = len(ents)
+
+ log.info("Try to delete a backend with suffix")
+ topology.standalone.backend.create(suffix=NEW_SUFFIX_1,
+ properties={BACKEND_NAME:
+ NEW_BACKEND_1})
+ topology.standalone.backend.delete(suffix=NEW_SUFFIX_1)
+ ents = topology.standalone.backend.list()
+ assert len(ents) == nb_backend
+
+ log.info("Try to delete a backend with backend name")
+ topology.standalone.backend.create(suffix=NEW_SUFFIX_1,
+ properties={BACKEND_NAME:
+ NEW_BACKEND_1})
+ topology.standalone.backend.delete(bename=NEW_BACKEND_1)
+ ents = topology.standalone.backend.list()
+ assert len(ents) == nb_backend
+
+ log.info("Try to delete a backend with backend DN")
+ topology.standalone.backend.create(suffix=NEW_SUFFIX_1,
+ properties={BACKEND_NAME:
+ NEW_BACKEND_1})
+ ents = topology.standalone.backend.list(suffix=NEW_SUFFIX_1)
+ assert len(ents) == 1
+ topology.standalone.backend.delete(backend_dn=ents[0].dn)
+ ents = topology.standalone.backend.list()
+ assert len(ents) == nb_backend
+
+
+def test_delete_invalid(topology):
+ """Test the invalid situations with the backend removal:
+ - no argument
+ - invalid suffix
+ - existing a mapping tree
+ - backend name differs
+ """
+
+ topology.standalone.backend.create(suffix=NEW_SUFFIX_1,
+ properties={BACKEND_NAME:
+ NEW_BACKEND_1})
+ topology.standalone.mappingtree.create(NEW_SUFFIX_1, bename=NEW_BACKEND_1)
+
+ log.info("First no argument -> UNWILLING_TO_PERFORM")
+ with pytest.raises(ldap.UNWILLING_TO_PERFORM) as excinfo:
+ topology.standalone.backend.delete()
+ assert 'suffix and backend DN and backend name are missing' in \
+ str(excinfo.value)
+
+ log.info("Second invalid suffix -> UNWILLING_TO_PERFORM")
+ with pytest.raises(ldap.UNWILLING_TO_PERFORM) as excinfo:
+ topology.standalone.backend.delete(suffix=NEW_SUFFIX_2)
+ assert 'Unable to retrieve the backend' in str(excinfo.value)
+
+ topology.standalone.mappingtree.delete(suffix=NEW_SUFFIX_1,
+ bename=NEW_BACKEND_1)
+
+ log.info("Backend name differs -> UNWILLING_TO_PERFORM")
+ with pytest.raises(ldap.UNWILLING_TO_PERFORM):
+ topology.standalone.backend.delete(suffix=NEW_SUFFIX_1,
bename='dummydb')
+ topology.standalone.backend.delete(suffix=NEW_SUFFIX_1)
+
+
+def test_toSuffix(topology):
+ """Test backend.toSuffix() function
+ by comparing its result to the true value
+ """
+
+ log.info("Create one backend")
+ topology.standalone.backend.create(suffix=NEW_SUFFIX_1,
+ properties={BACKEND_NAME:
+ NEW_BACKEND_1})
+
+ log.info("Run through all backends and compare backend.toSuffix() "
+ "function results with true values taken from attributes")
+ ents = topology.standalone.backend.list()
+ for ent in ents:
+ suffix = ent.getValues(BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_SUFFIX])
+ values = topology.standalone.backend.toSuffix(name=ent.dn)
+ assert suffix[0] in values
+
+ log.info("Clean up after test")
+ topology.standalone.backend.delete(suffix=NEW_SUFFIX_1)
+
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/cli/__init__.py
b/src/lib389/lib389/tests/cli/__init__.py
new file mode 100644
index 0000000..02cfc51
--- /dev/null
+++ b/src/lib389/lib389/tests/cli/__init__.py
@@ -0,0 +1,104 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import pytest
+
+from lib389 import DirSrv
+from lib389.cli_base import LogCapture, FakeArgs
+
+from lib389.instance.setup import SetupDs
+from lib389.instance.options import General2Base, Slapd2Base
+from lib389._constants import *
+
+from lib389.configurations import get_sample_entries
+
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+
+DEBUGGING = True
+
+class TopologyInstance(object):
+ def __init__(self, standalone, logcap):
+ # For these tests, we don't want to open the instance.
+ # instance.open()
+ self.standalone = standalone
+ self.logcap = logcap
+
+# Need a teardown to destroy the instance.
+(a)pytest.fixture(scope="module")
+def topology(request):
+ lc = LogCapture()
+ instance = DirSrv(verbose=DEBUGGING)
+ instance.log.debug("Instance allocated")
+ args = {SER_PORT: INSTANCE_PORT,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ instance.allocate(args)
+ if instance.exists():
+ instance.delete()
+
+ # This will need to change to instance.create in the future
+ # when it's linked up!
+ sds = SetupDs(verbose=DEBUGGING, dryrun=False, log=lc.log)
+
+ # Get the dicts from Type2Base, as though they were from _validate_ds_2_config
+ # IE get the defaults back just from Slapd2Base.collect
+ # Override instance name, root password, port and secure port.
+
+ general_options = General2Base(lc.log)
+ general_options.verify()
+ general = general_options.collect()
+
+ # Need an args -> options2 ...
+ slapd_options = Slapd2Base(lc.log)
+ slapd_options.set('instance_name', INSTANCE_SERVERID)
+ slapd_options.set('port', INSTANCE_PORT)
+ slapd_options.set('root_password', PW_DM)
+ slapd_options.verify()
+ slapd = slapd_options.collect()
+
+ sds.create_from_args(general, slapd, {}, None)
+ insts = instance.list(serverid=INSTANCE_SERVERID)
+ # Assert we did change the system.
+ assert(len(insts) == 1)
+ # Make sure we can connect
+ instance.open(connOnly=True)
+
+ def fin():
+ if instance.exists() and not DEBUGGING:
+ instance.delete()
+ request.addfinalizer(fin)
+
+ return TopologyInstance(instance, lc)
+
+
+(a)pytest.fixture(scope="module")
+def topology_be_latest(topology):
+ be = topology.standalone.backends.create(properties={
+ 'cn': 'userRoot',
+ 'suffix' : DEFAULT_SUFFIX,
+ })
+ # Now apply sample entries
+ centries = get_sample_entries(INSTALL_LATEST_CONFIG)
+ cent = centries(topology.standalone, DEFAULT_SUFFIX)
+ cent.apply()
+ return topology
+
+
+(a)pytest.fixture(scope="module")
+def topology_be_001003006(topology):
+ be = topology.standalone.backends.create(properties={
+ 'cn': 'userRoot',
+ 'suffix' : DEFAULT_SUFFIX,
+ })
+ # Now apply sample entries
+ centries = get_sample_entries('001003006')
+ cent = centries(topology.standalone, DEFAULT_SUFFIX)
+ cent.apply()
+ return topology
+
+
diff --git a/src/lib389/lib389/tests/cli/adm_instance_test.py
b/src/lib389/lib389/tests/cli/adm_instance_test.py
new file mode 100644
index 0000000..1c3d829
--- /dev/null
+++ b/src/lib389/lib389/tests/cli/adm_instance_test.py
@@ -0,0 +1,31 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# Test the cli tools from the dsadm command for correct behaviour.
+
+import pytest
+from lib389.cli_ctl.instance import instance_list
+from lib389 import DirSrv
+from lib389.cli_base import LogCapture
+
+
+
+def test_instance_list():
+ lc = LogCapture()
+ inst = DirSrv()
+ instance_list(inst, lc.log, None)
+ assert(lc.contains("No instances of Directory Server") or
lc.contains("instance: "))
+ # Now assert the logs in the capture.
+
+# Need a fixture to install an instance.
+
+# Test start
+
+# Test stop
+
+
diff --git a/src/lib389/lib389/tests/cli/conf_backend_test.py
b/src/lib389/lib389/tests/cli/conf_backend_test.py
new file mode 100644
index 0000000..da8ec73
--- /dev/null
+++ b/src/lib389/lib389/tests/cli/conf_backend_test.py
@@ -0,0 +1,52 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import pytest
+
+from lib389.cli_conf.backend import backend_list, backend_get, backend_get_dn,
backend_create, backend_delete
+
+from lib389.cli_base import LogCapture, FakeArgs
+from lib389.tests.cli import topology
+
+# Topology is pulled from __init__.py
+def test_backend_cli(topology):
+ #
+ args = FakeArgs()
+ backend_list(topology.standalone, None, topology.logcap.log, None)
+ # Assert none.
+ assert(topology.logcap.contains("No objects to display"))
+ topology.logcap.flush()
+ # Add a backend
+ # We need to fake the args
+ args.extra = ['dc=example,dc=com', 'userRoot']
+ backend_create(topology.standalone, None, topology.logcap.log, args)
+ # Assert one.
+ backend_list(topology.standalone, None, topology.logcap.log, None)
+ # Assert none.
+ assert(topology.logcap.contains("userRoot"))
+ topology.logcap.flush()
+ # Assert we can get by name, suffix, dn
+ args.selector = 'userRoot'
+ backend_get(topology.standalone, None, topology.logcap.log, args)
+ # Assert none.
+ assert(topology.logcap.contains("userRoot"))
+ topology.logcap.flush()
+ # Assert we can get by name, suffix, dn
+ args.dn = 'cn=userRoot,cn=ldbm database,cn=plugins,cn=config'
+ backend_get_dn(topology.standalone, None, topology.logcap.log, args)
+ # Assert none.
+ assert(topology.logcap.contains("userRoot"))
+ topology.logcap.flush()
+ # delete it
+ backend_delete(topology.standalone, None, topology.logcap.log, args, warn=False)
+ backend_list(topology.standalone, None, topology.logcap.log, None)
+ # Assert none.
+ assert(topology.logcap.contains("No objects to display"))
+ topology.logcap.flush()
+ # Done!
+
diff --git a/src/lib389/lib389/tests/cli/conf_plugin_test.py
b/src/lib389/lib389/tests/cli/conf_plugin_test.py
new file mode 100644
index 0000000..3f1955f
--- /dev/null
+++ b/src/lib389/lib389/tests/cli/conf_plugin_test.py
@@ -0,0 +1,53 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import pytest
+
+from lib389.cli_conf.plugin import plugin_list, plugin_get, plugin_get_dn, plugin_enable,
plugin_disable
+
+from lib389.cli_base import LogCapture, FakeArgs
+
+from lib389.tests.cli import topology
+
+plugins = [
+ 'Class of Service',
+ 'ldbm database',
+ 'Roles Plugin',
+ 'USN',
+ 'SSHA512',
+]
+
+# Topology is pulled from __init__.py
+def test_plugin_cli(topology):
+ args = FakeArgs()
+
+ plugin_list(topology.standalone, None, topology.logcap.log, None)
+ for p in plugins:
+ assert(topology.logcap.contains(p))
+ topology.logcap.flush()
+
+ # print(topology.logcap.outputs)
+ # Need to delete something, then re-add it.
+ args.selector = 'USN'
+ plugin_get(topology.standalone, None, topology.logcap.log, args)
+ assert(topology.logcap.contains('USN'))
+ topology.logcap.flush()
+
+ args.dn = 'cn=USN,cn=plugins,cn=config'
+ plugin_get_dn(topology.standalone, None, topology.logcap.log, args)
+ assert(topology.logcap.contains('USN'))
+ topology.logcap.flush()
+
+ plugin_disable(topology.standalone, None, topology.logcap.log, args, warn=False)
+ assert(topology.logcap.contains('Disabled'))
+ topology.logcap.flush()
+
+ plugin_enable(topology.standalone, None, topology.logcap.log, args)
+ assert(topology.logcap.contains('Enabled'))
+ topology.logcap.flush()
+
diff --git a/src/lib389/lib389/tests/cli/conf_plugins/__init__.py
b/src/lib389/lib389/tests/cli/conf_plugins/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib389/lib389/tests/cli/conf_plugins/memberof_test.py
b/src/lib389/lib389/tests/cli/conf_plugins/memberof_test.py
new file mode 100644
index 0000000..339138f
--- /dev/null
+++ b/src/lib389/lib389/tests/cli/conf_plugins/memberof_test.py
@@ -0,0 +1,490 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016-2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import pytest
+
+from lib389.tests.cli import topology as default_topology
+from lib389.cli_base import LogCapture, FakeArgs
+from lib389.plugins import MemberOfPlugin
+from lib389.cli_conf.plugins import memberof as memberof_cli
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ topology = default_topology(request)
+
+ plugin = MemberOfPlugin(topology.standalone)
+ if not plugin.exists():
+ plugin.create()
+
+ # At the moment memberof plugin needs to be enabled in order to perform
+ # syntax checking. Additionally, we have to restart the server in order
+ # for the action of enabling the plugin to take effect.
+ plugin.enable()
+ topology.standalone.restart()
+ topology.logcap.flush()
+
+ return topology
+
+
+def test_set_attr_with_legal_value(topology):
+ args = FakeArgs()
+
+ args.value = "memberOf"
+ memberof_cli.manage_attr(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("memberOfAttr set to")
+ topology.logcap.flush()
+
+ args.value = None
+ memberof_cli.manage_attr(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains(": memberOf")
+ topology.logcap.flush()
+
+def test_set_attr_with_illegal_value(topology):
+ args = FakeArgs()
+
+ args.value = "whatever"
+ memberof_cli.manage_attr(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("Failed to set")
+ topology.logcap.flush()
+
+def test_set_groupattr_with_legal_value(topology):
+ args = FakeArgs()
+
+ args.value = "uniqueMember"
+ memberof_cli.add_groupattr(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("successfully added memberOfGroupAttr
value")
+ topology.logcap.flush()
+
+ memberof_cli.display_groupattr(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains(": uniqueMember")
+ topology.logcap.flush()
+
+def test_set_groupattr_with_value_that_already_exists(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # setup test
+ if not "uniqueMember" in plugin.get_groupattr():
+ plugin.add_groupattr("uniqueMember")
+
+ args = FakeArgs()
+
+ args.value = "uniqueMember"
+ memberof_cli.add_groupattr(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("already exists")
+ topology.logcap.flush()
+
+def test_set_groupattr_with_illegal_value(topology):
+ args = FakeArgs()
+
+ args.value = "whatever"
+ memberof_cli.add_groupattr(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("Illegal value")
+ topology.logcap.flush()
+
+def test_remove_groupattr_with_value_that_exists(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # setup test
+ if not "uniqueMember" in plugin.get_groupattr():
+ plugin.add_groupattr("uniqueMember")
+
+ args = FakeArgs()
+
+ args.value = "uniqueMember"
+ memberof_cli.remove_groupattr(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("successfully removed memberOfGroupAttr
value")
+ topology.logcap.flush()
+
+ memberof_cli.display_groupattr(topology.standalone, None, topology.logcap.log, args)
+ assert not topology.logcap.contains(": uniqueMember")
+ topology.logcap.flush()
+
+def test_remove_groupattr_with_value_that_doesnt_exist(topology):
+ args = FakeArgs()
+
+ args.value = "whatever"
+ memberof_cli.remove_groupattr(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains('No value "{0}"
found'.format(args.value))
+ topology.logcap.flush()
+
+def test_try_remove_all_groupattr_values(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # make sure "member" value exists and it is the only one
+ assert "member" in plugin.get_groupattr() # exists from default
+ assert len(plugin.get_groupattr()) == 1
+
+ args = FakeArgs()
+
+ args.value = "member"
+ memberof_cli.remove_groupattr(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("Error: Failed to delete. memberOfGroupAttr is
required.")
+ topology.logcap.flush()
+
+def test_get_allbackends_when_not_set(topology):
+ args = FakeArgs()
+
+ memberof_cli.display_allbackends(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("memberOfAllBackends is not set")
+ topology.logcap.flush()
+
+def test_enable_allbackends(topology):
+ args = FakeArgs()
+
+ memberof_cli.enable_allbackends(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("enabled successfully")
+ topology.logcap.flush()
+
+ memberof_cli.display_allbackends(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains(": on")
+ topology.logcap.flush()
+
+def test_disable_all_backends(topology):
+ args = FakeArgs()
+
+ memberof_cli.disable_allbackends(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("disabled successfully")
+ topology.logcap.flush()
+
+ memberof_cli.display_allbackends(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains(": off")
+ topology.logcap.flush()
+
+def test_get_skipnested_when_not_set(topology):
+ args = FakeArgs()
+
+ memberof_cli.display_skipnested(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("memberOfSkipNested is not set")
+ topology.logcap.flush()
+
+def test_set_skipnested(topology):
+ args = FakeArgs()
+
+ memberof_cli.enable_skipnested(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("set successfully")
+ topology.logcap.flush()
+
+ memberof_cli.display_skipnested(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains(": on")
+ topology.logcap.flush()
+
+def test_unset_skipnested(topology):
+ args = FakeArgs()
+
+ memberof_cli.disable_skipnested(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("unset successfully")
+ topology.logcap.flush()
+
+ memberof_cli.display_skipnested(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains(": off")
+ topology.logcap.flush()
+
+def test_set_autoaddoc(topology):
+ args = FakeArgs()
+
+ # argparse makes sure that only choices 'nsmemberof', 'inetuser',
'inetadmin', 'del' are allowed
+ args.value = "nsmemberof"
+ memberof_cli.manage_autoaddoc(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("memberOfAutoAddOc set to")
+ topology.logcap.flush()
+
+ args.value = None
+ memberof_cli.manage_autoaddoc(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains(": nsMemberOf")
+ topology.logcap.flush()
+
+def test_remove_autoaddoc(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # setup test
+ if not plugin.get_autoaddoc():
+ plugin.set_autoaddoc("nsmemberof")
+
+ args = FakeArgs()
+
+ # argparse makes sure that only choices 'nsmemberof', 'inetuser',
'inetadmin', 'del' are allowed
+ args.value = "del"
+ memberof_cli.manage_autoaddoc(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("memberOfAutoAddOc attribute deleted")
+ topology.logcap.flush()
+
+ args.value = None
+ memberof_cli.manage_autoaddoc(topology.standalone, None, topology.logcap.log, args)
+ assert not topology.logcap.contains(": nsMemberOf")
+ topology.logcap.flush()
+
+def test_remove_autoaddoc_when_not_set(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # setup test
+ if plugin.get_autoaddoc():
+ plugin.remove_autoaddoc()
+
+ args = FakeArgs()
+
+ args.value = "del"
+ memberof_cli.manage_autoaddoc(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("memberOfAutoAddOc was not set")
+ topology.logcap.flush()
+
+def test_get_autoaddoc_when_not_set(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # setup test
+ if plugin.get_autoaddoc():
+ plugin.remove_autoaddoc()
+
+ args = FakeArgs()
+
+ args.value = None
+ memberof_cli.manage_autoaddoc(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("memberOfAutoAddOc is not set")
+ topology.logcap.flush()
+
+def test_get_entryscope_when_not_set(topology):
+ args = FakeArgs()
+
+ args.value = None
+ memberof_cli.display_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("memberOfEntryScope is not set")
+ topology.logcap.flush()
+
+def test_add_entryscope_with_legal_value(topology):
+ args = FakeArgs()
+
+ args.value = "dc=example,dc=com"
+ memberof_cli.add_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("successfully added memberOfEntryScope
value")
+ topology.logcap.flush()
+
+ args.value = None
+ memberof_cli.display_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains(": dc=example,dc=com")
+ topology.logcap.flush()
+
+ args.value = "a=b"
+ memberof_cli.add_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("successfully added memberOfEntryScope
value")
+ topology.logcap.flush()
+
+ args.value = None
+ memberof_cli.display_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains(": dc=example,dc=com")
+ assert topology.logcap.contains(": a=b")
+ topology.logcap.flush()
+
+def test_add_entryscope_with_illegal_value(topology):
+ args = FakeArgs()
+
+ args.value = "whatever"
+ memberof_cli.add_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("Error: Invalid DN")
+ topology.logcap.flush()
+
+def test_add_entryscope_with_existing_value(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # setup test
+ if not "dc=example,dc=com" in plugin.get_entryscope():
+ plugin.add_entryscope("dc=example,dc=com")
+
+ args = FakeArgs()
+
+ args.value = "dc=example,dc=com"
+ memberof_cli.add_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains('Value "{}" already
exists'.format(args.value))
+ topology.logcap.flush()
+
+def test_remove_entryscope_with_existing_value(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # setup test
+ if not "a=b" in plugin.get_entryscope():
+ plugin.add_entryscope("a=b")
+ if not "dc=example,dc=com" in plugin.get_entryscope():
+ plugin.add_entryscope("dc=example,dc=com")
+
+ args = FakeArgs()
+
+ args.value = "a=b"
+ memberof_cli.remove_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("successfully removed memberOfEntryScope
value")
+ topology.logcap.flush()
+
+ args.value = None
+ memberof_cli.display_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains(": dc=example,dc=com")
+ assert not topology.logcap.contains(": a=b")
+ topology.logcap.flush()
+
+def test_remove_entryscope_with_non_existing_value(topology):
+ args = FakeArgs()
+
+ args.value = "whatever"
+ memberof_cli.remove_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains('No value "{0}"
found'.format(args.value))
+ topology.logcap.flush()
+
+def test_remove_all_entryscope(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # setup test
+ if not "dc=example,dc=com" in plugin.get_entryscope():
+ plugin.add_entryscope("dc=example,dc=com")
+ if not "a=b" in plugin.get_entryscope():
+ plugin.add_entryscope("a=b")
+
+ args = FakeArgs()
+
+ args.value = None
+ memberof_cli.display_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains(": a=b")
+ assert topology.logcap.contains(": dc=example,dc=com")
+ topology.logcap.flush()
+
+ args.value = None
+ memberof_cli.remove_all_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("successfully removed all memberOfEntryScope
values")
+ topology.logcap.flush()
+
+ args.value = None
+ memberof_cli.display_scope(topology.standalone, None, topology.logcap.log, args)
+ assert not topology.logcap.contains(": a=b")
+ assert not topology.logcap.contains(": dc=example,dc=com")
+ topology.logcap.flush()
+
+def test_get_excludescope_when_not_set(topology):
+ args = FakeArgs()
+
+ args.value = None
+ memberof_cli.display_excludescope(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("memberOfEntryScopeExcludeSubtree is not
set")
+ topology.logcap.flush()
+
+def test_add_excludescope_with_legal_value(topology):
+ args = FakeArgs()
+
+ args.value = "ou=People,dc=example,dc=com"
+ memberof_cli.add_excludescope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("successfully added
memberOfEntryScopeExcludeSubtree value")
+ topology.logcap.flush()
+
+ args.value = None
+ memberof_cli.display_excludescope(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains(": ou=People,dc=example,dc=com")
+ topology.logcap.flush()
+
+ args.value = "a=b"
+ memberof_cli.add_excludescope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("successfully added
memberOfEntryScopeExcludeSubtree value")
+ topology.logcap.flush()
+
+ args.value = None
+ memberof_cli.display_excludescope(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains(": ou=People,dc=example,dc=com")
+ assert topology.logcap.contains(": a=b")
+ topology.logcap.flush()
+
+def test_add_excludescope_with_illegal_value(topology):
+ args = FakeArgs()
+
+ args.value = "whatever"
+ memberof_cli.add_excludescope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("Error: Invalid DN")
+ topology.logcap.flush()
+
+def test_add_excludescope_with_existing_value(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # setup test
+ if not "ou=People,dc=example,dc=com" in plugin.get_excludescope():
+ plugin.add_excludescope("ou=People,dc=example,dc=com")
+
+ args = FakeArgs()
+
+ args.value = "ou=People,dc=example,dc=com"
+ memberof_cli.add_excludescope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains('Value "{}" already
exists'.format(args.value))
+ topology.logcap.flush()
+
+def test_remove_excludescope_with_existing_value(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # setup test
+ if not "a=b" in plugin.get_excludescope():
+ plugin.add_excludescope("a=b")
+ if not "ou=People,dc=example,dc=com" in plugin.get_excludescope():
+ plugin.add_excludescope("ou=People,dc=example,dc=com")
+
+ args = FakeArgs()
+
+ args.value = "a=b"
+ memberof_cli.remove_excludescope(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("successfully removed
memberOfEntryScopeExcludeSubtree value")
+ topology.logcap.flush()
+
+ args.value = None
+ memberof_cli.display_excludescope(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains(": ou=People,dc=example,dc=com")
+ assert not topology.logcap.contains(": a=b")
+ topology.logcap.flush()
+
+def test_remove_excludescope_with_non_existing_value(topology):
+ args = FakeArgs()
+
+ args.value = "whatever"
+ memberof_cli.remove_excludescope(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains('No value "{0}"
found'.format(args.value))
+ topology.logcap.flush()
+
+def test_remove_all_excludescope(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # setup test
+ if not "a=b" in plugin.get_excludescope():
+ plugin.add_excludescope("a=b")
+ if not "ou=People,dc=example,dc=com" in plugin.get_excludescope():
+ plugin.add_excludescope("ou=People,dc=example,dc=com")
+
+ args = FakeArgs()
+
+ args.value = None
+ memberof_cli.display_excludescope(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains(": a=b")
+ assert topology.logcap.contains(": ou=People,dc=example,dc=com")
+ topology.logcap.flush()
+
+ args.value = None
+ memberof_cli.remove_all_excludescope(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("successfully removed all
memberOfEntryScopeExcludeSubtree values")
+ topology.logcap.flush()
+
+ args.value = None
+ memberof_cli.display_excludescope(topology.standalone, None, topology.logcap.log,
args)
+ assert not topology.logcap.contains(": a=b")
+ assert not topology.logcap.contains(": ou=People,dc=example,dc=com")
+ topology.logcap.flush()
+
+def test_add_entryscope_with_value_that_exists_in_excludescope(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # setup test
+ if not "dc=example,dc=com" in plugin.get_entryscope():
+ plugin.add_entryscope("dc=example,dc=com")
+ if not "ou=People,dc=example,dc=com" in plugin.get_excludescope():
+ plugin.add_excludescope("ou=People,dc=example,dc=com")
+
+ args = FakeArgs()
+
+ args.value = "ou=People,dc=example,dc=com"
+ memberof_cli.add_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("is also listed as an exclude suffix")
+ topology.logcap.flush()
+
+def test_add_excludescope_with_value_that_exists_in_entryscope(topology):
+ plugin = MemberOfPlugin(topology.standalone)
+ # setup test
+ if not "dc=example,dc=com" in plugin.get_entryscope():
+ plugin.add_entryscope("dc=example,dc=com")
+ if not "ou=People,dc=example,dc=com" in plugin.get_excludescope():
+ plugin.add_excludescope("ou=People,dc=example,dc=com")
+
+ args = FakeArgs()
+
+ args.value = "ou=People,dc=example,dc=com"
+ memberof_cli.add_scope(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("is also listed as an exclude suffix")
+ topology.logcap.flush()
diff --git a/src/lib389/lib389/tests/cli/conf_plugins/referint_test.py
b/src/lib389/lib389/tests/cli/conf_plugins/referint_test.py
new file mode 100644
index 0000000..8a31ffc
--- /dev/null
+++ b/src/lib389/lib389/tests/cli/conf_plugins/referint_test.py
@@ -0,0 +1,119 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016-2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import pytest
+
+from lib389.tests.cli import topology as default_topology
+from lib389.cli_base import LogCapture, FakeArgs
+from lib389.plugins import ReferentialIntegrityPlugin
+from lib389.cli_conf.plugins import referint as referint_cli
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ topology = default_topology(request)
+
+ plugin = ReferentialIntegrityPlugin(topology.standalone)
+ if not plugin.exists():
+ plugin.create()
+
+ # we need to restart the server after enabling the plugin
+ plugin.enable()
+ topology.standalone.restart()
+ topology.logcap.flush()
+
+ return topology
+
+
+def test_set_update_delay(topology):
+ args = FakeArgs()
+
+ args.value = 60
+ referint_cli.manage_update_delay(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains('referint-update-delay set to
"60"')
+ topology.logcap.flush()
+
+ args.value = None
+ referint_cli.manage_update_delay(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("referint-update-delay: 60")
+ topology.logcap.flush()
+
+ args.value = 0
+ referint_cli.manage_update_delay(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains('referint-update-delay set to
"0"')
+ topology.logcap.flush()
+
+ args.value = None
+ referint_cli.manage_update_delay(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("referint-update-delay: 0")
+ topology.logcap.flush()
+
+def test_add_membership_attr(topology):
+ args = FakeArgs()
+
+ args.value = "member2"
+ referint_cli.add_membership_attr(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("successfully added membership attribute")
+ topology.logcap.flush()
+
+ referint_cli.display_membership_attr(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains(": member2")
+ topology.logcap.flush()
+
+def test_add_membership_attr_with_value_that_already_exists(topology):
+ plugin = ReferentialIntegrityPlugin(topology.standalone)
+ # setup test
+ if not "uniqueMember" in plugin.get_membership_attr():
+ plugin.add_membership_attr("uniqueMember")
+
+ args = FakeArgs()
+
+ args.value = "uniqueMember"
+ referint_cli.add_membership_attr(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("already exists")
+ topology.logcap.flush()
+
+def test_remove_membership_attr_with_value_that_exists(topology):
+ plugin = ReferentialIntegrityPlugin(topology.standalone)
+ # setup test
+ if not "uniqueMember" in plugin.get_membership_attr():
+ plugin.add_membership_attr("uniqueMember")
+
+ args = FakeArgs()
+
+ args.value = "uniqueMember"
+ referint_cli.remove_membership_attr(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("successfully removed membership
attribute")
+ topology.logcap.flush()
+
+ referint_cli.display_membership_attr(topology.standalone, None, topology.logcap.log,
args)
+ assert not topology.logcap.contains(": uniqueMember")
+ topology.logcap.flush()
+
+def test_remove_membership_attr_with_value_that_doesnt_exist(topology):
+ args = FakeArgs()
+
+ args.value = "whatever"
+ referint_cli.remove_membership_attr(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains('No value "{0}"
found'.format(args.value))
+ topology.logcap.flush()
+
+def test_try_remove_all_membership_attr_values(topology):
+ plugin = ReferentialIntegrityPlugin(topology.standalone)
+ #setup test
+ membership_values = plugin.get_membership_attr()
+ assert len(membership_values) > 0
+ for val in membership_values[:-1]:
+ plugin.remove_membership_attr(val)
+
+ args = FakeArgs()
+
+ args.value = membership_values[-1]
+ referint_cli.remove_membership_attr(topology.standalone, None, topology.logcap.log,
args)
+ assert topology.logcap.contains("Error: Failed to delete. At least one value for
membership attribute should exist.")
+ topology.logcap.flush()
diff --git a/src/lib389/lib389/tests/cli/conf_plugins/rootdn_ac_test.py
b/src/lib389/lib389/tests/cli/conf_plugins/rootdn_ac_test.py
new file mode 100644
index 0000000..76650b2
--- /dev/null
+++ b/src/lib389/lib389/tests/cli/conf_plugins/rootdn_ac_test.py
@@ -0,0 +1,281 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016-2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import pytest
+
+from lib389.tests.cli import topology as default_topology
+from lib389.cli_base import LogCapture, FakeArgs
+from lib389.plugins import RootDNAccessControlPlugin
+from lib389.cli_conf.plugins import rootdn_ac as rootdn_cli
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ topology = default_topology(request)
+
+ plugin = RootDNAccessControlPlugin(topology.standalone)
+ if not plugin.exists():
+ plugin.create()
+
+ # we need to restart the server after enabling the plugin
+ plugin.enable()
+ topology.standalone.restart()
+ topology.logcap.flush()
+
+ return topology
+
+def test_set_open_time(topology):
+ args = FakeArgs()
+
+ args.value = "1030"
+ rootdn_cli.set_open_time(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-open-time set to")
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_time(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-open-time: 1030")
+ topology.logcap.flush()
+
+def test_set_close_time(topology):
+ args = FakeArgs()
+
+ args.value = "1545"
+ rootdn_cli.set_close_time(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-close-time set to")
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_time(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-close-time: 1545")
+ topology.logcap.flush()
+
+def test_clear_time(topology):
+ args = FakeArgs()
+
+ rootdn_cli.clear_time(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("time-based policy was cleared")
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_time(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-open-time is not set")
+ assert topology.logcap.contains("rootdn-close-time is not set")
+ topology.logcap.flush()
+
+def test_allow_ip(topology):
+ args = FakeArgs()
+
+ args.value = "127.0.0.1"
+ rootdn_cli.allow_ip(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("{} added to
rootdn-allow-ip".format(args.value))
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_ips(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-allow-ip: 127.0.0.1")
+ topology.logcap.flush()
+
+def test_deny_ip(topology):
+ args = FakeArgs()
+
+ args.value = "127.0.0.2"
+ rootdn_cli.deny_ip(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("{} added to
rootdn-deny-ip".format(args.value))
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_ips(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-deny-ip: 127.0.0.2")
+ topology.logcap.flush()
+
+def test_when_ip_is_allowed_its_not_denied(topology):
+ args = FakeArgs()
+
+ args.value = "127.0.0.3"
+ rootdn_cli.deny_ip(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("{} added to
rootdn-deny-ip".format(args.value))
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_ips(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-deny-ip: 127.0.0.3")
+ topology.logcap.flush()
+
+ args.value = "127.0.0.3"
+ rootdn_cli.allow_ip(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("{} added to
rootdn-allow-ip".format(args.value))
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_ips(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-allow-ip: 127.0.0.3")
+ assert not topology.logcap.contains("rootdn-deny-ip: 127.0.0.3")
+ topology.logcap.flush()
+
+def test_when_ip_is_denied_its_not_allowed(topology):
+ args = FakeArgs()
+
+ args.value = "127.0.0.4"
+ rootdn_cli.allow_ip(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("{} added to
rootdn-allow-ip".format(args.value))
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_ips(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-allow-ip: 127.0.0.4")
+ topology.logcap.flush()
+
+ args.value = "127.0.0.4"
+ rootdn_cli.deny_ip(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("{} added to
rootdn-deny-ip".format(args.value))
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_ips(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-deny-ip: 127.0.0.4")
+ assert not topology.logcap.contains("rootdn-allow-ip: 127.0.0.4")
+ topology.logcap.flush()
+
+def test_clear_ips(topology):
+ args = FakeArgs()
+
+ rootdn_cli.clear_all_ips(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("ip-based policy was cleared")
+ topology.logcap.flush()
+
+def test_allow_host(topology):
+ args = FakeArgs()
+
+ args.value = "example1.com"
+ rootdn_cli.allow_host(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("{} added to
rootdn-allow-host".format(args.value))
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_hosts(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-allow-host: example1.com")
+ topology.logcap.flush()
+
+def test_deny_host(topology):
+ args = FakeArgs()
+
+ args.value = "example2.com"
+ rootdn_cli.deny_host(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("{} added to
rootdn-deny-host".format(args.value))
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_hosts(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-deny-host: example2.com")
+ topology.logcap.flush()
+
+def test_when_host_is_allowed_its_not_denied(topology):
+ args = FakeArgs()
+
+ args.value = "example3.com"
+ rootdn_cli.deny_host(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("{} added to
rootdn-deny-host".format(args.value))
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_hosts(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-deny-host: example3.com")
+ topology.logcap.flush()
+
+ args.value = "example3.com"
+ rootdn_cli.allow_host(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("{} added to
rootdn-allow-host".format(args.value))
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_hosts(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-allow-host: example3.com")
+ assert not topology.logcap.contains("rootdn-deny-host: example3.com")
+ topology.logcap.flush()
+
+def test_when_host_is_denied_its_not_allowed(topology):
+ args = FakeArgs()
+
+ args.value = "example4.com"
+ rootdn_cli.allow_host(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("{} added to
rootdn-allow-host".format(args.value))
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_hosts(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-allow-host: example4.com")
+ topology.logcap.flush()
+
+ args.value = "example4.com"
+ rootdn_cli.deny_host(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("{} added to
rootdn-deny-host".format(args.value))
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_hosts(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-deny-host: example4.com")
+ assert not topology.logcap.contains("rootdn-allow-host: example4.com")
+ topology.logcap.flush()
+
+def test_clear_hosts(topology):
+ args = FakeArgs()
+
+ rootdn_cli.clear_all_hosts(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("host-based policy was cleared")
+ topology.logcap.flush()
+
+def test_allow_and_deny_days(topology):
+ args = FakeArgs()
+
+ args.value = "Mon".capitalize()
+ rootdn_cli.allow_day(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("{} added to
rootdn-days-allowed".format(args.value))
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_days(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-days-allowed: Mon")
+ topology.logcap.flush()
+
+ args.value = "friday".capitalize()
+ rootdn_cli.allow_day(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("Fri added to rootdn-days-allowed")
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_days(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-days-allowed: Mon, Fri")
+ topology.logcap.flush()
+
+ args.value = "MONDAY".capitalize()
+ rootdn_cli.deny_day(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("Mon removed from rootdn-days-allowed")
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_days(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("rootdn-days-allowed: Fri")
+ topology.logcap.flush()
+
+ args.value = "fri".capitalize()
+ rootdn_cli.deny_day(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("Fri removed from rootdn-days-allowed")
+ topology.logcap.flush()
+
+ args.value = None
+ rootdn_cli.display_days(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("No day-based access control policy has been
configured")
+ topology.logcap.flush()
+
+def test_clear_days(topology):
+ args = FakeArgs()
+
+ rootdn_cli.clear_all_days(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("day-based policy was cleared")
+ topology.logcap.flush()
diff --git a/src/lib389/lib389/tests/cli/conf_plugins/usn_test.py
b/src/lib389/lib389/tests/cli/conf_plugins/usn_test.py
new file mode 100644
index 0000000..fcfc95f
--- /dev/null
+++ b/src/lib389/lib389/tests/cli/conf_plugins/usn_test.py
@@ -0,0 +1,53 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016-2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import pytest
+
+from lib389.tests.cli import topology as default_topology
+from lib389.cli_base import LogCapture, FakeArgs
+from lib389.plugins import USNPlugin
+from lib389.cli_conf.plugins import usn as usn_cli
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ topology = default_topology(request)
+
+ plugin = USNPlugin(topology.standalone)
+ if not plugin.exists():
+ plugin.create()
+
+ # we need to restart the server after enabling the plugin
+ plugin.enable()
+ topology.standalone.restart()
+ topology.logcap.flush()
+
+ return topology
+
+
+def test_enable_global_mode(topology):
+ args = FakeArgs()
+
+ usn_cli.enable_global_mode(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("USN global mode enabled")
+ topology.logcap.flush()
+
+ usn_cli.display_usn_mode(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("USN global mode is enabled")
+ topology.logcap.flush()
+
+def test_disable_global_mode(topology):
+ args = FakeArgs()
+
+ usn_cli.disable_global_mode(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("USN global mode disabled")
+ topology.logcap.flush()
+
+ usn_cli.display_usn_mode(topology.standalone, None, topology.logcap.log, args)
+ assert topology.logcap.contains("USN global mode is disabled")
+ topology.logcap.flush()
diff --git a/src/lib389/lib389/tests/cli/ctl_dbtasks_test.py
b/src/lib389/lib389/tests/cli/ctl_dbtasks_test.py
new file mode 100644
index 0000000..f2c31e5
--- /dev/null
+++ b/src/lib389/lib389/tests/cli/ctl_dbtasks_test.py
@@ -0,0 +1,71 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# Test the cli tools from the dsctl command for correct behaviour.
+
+import os
+import pytest
+from lib389.cli_ctl.dbtasks import dbtasks_db2index, dbtasks_db2bak, dbtasks_db2ldif,
dbtasks_ldif2db, dbtasks_bak2db
+
+from lib389.cli_base import LogCapture, FakeArgs
+from lib389.tests.cli import topology, topology_be_latest
+
+def test_db2index(topology):
+ pass
+
+def test_db2bak_bak2db(topology_be_latest):
+ standalone = topology_be_latest.standalone
+ standalone.stop()
+ args = FakeArgs()
+ args.archive = os.path.join(standalone.get_bak_dir(), "testdb2bak")
+ # Stop the instance
+ dbtasks_db2bak(standalone, topology_be_latest.logcap.log, args)
+ # Assert none.
+ assert topology_be_latest.logcap.contains("db2bak successful")
+ topology_be_latest.logcap.flush()
+ # We can re-use the same arguments
+ dbtasks_bak2db(standalone, topology_be_latest.logcap.log, args)
+ # Assert none.
+ assert topology_be_latest.logcap.contains("bak2db successful")
+
+def test_ldif2db_db2ldif_no_repl(topology_be_latest):
+ standalone = topology_be_latest.standalone
+ standalone.stop()
+ args = FakeArgs()
+ args.backend = 'userRoot'
+ args.ldif = os.path.join(standalone.get_ldif_dir(), "test.ldif")
+ args.encrypted = False
+ args.replication = False
+ # Stop the instance
+ dbtasks_db2ldif(standalone, topology_be_latest.logcap.log, args)
+ # Assert none.
+ assert topology_be_latest.logcap.contains("db2ldif successful")
+ topology_be_latest.logcap.flush()
+ # We can re-use the same arguments
+ dbtasks_ldif2db(standalone, topology_be_latest.logcap.log, args)
+ # Assert none.
+ assert topology_be_latest.logcap.contains("ldif2db successful")
+
+def test_ldif2db_db2ldif_repl(topology_be_latest):
+ standalone = topology_be_latest.standalone
+ standalone.stop()
+ args = FakeArgs()
+ args.backend = 'userRoot'
+ args.ldif = os.path.join(standalone.get_ldif_dir(), "test.ldif")
+ args.encrypted = False
+ args.replication = False
+ args.archive = os.path.join(standalone.get_ldif_dir(), "test.ldif")
+ # Stop the instance
+ dbtasks_db2ldif(standalone, topology_be_latest.logcap.log, args)
+ # Assert none.
+ assert topology_be_latest.logcap.contains("db2ldif successful")
+ topology_be_latest.logcap.flush()
+ # We can re-use the same arguments
+ dbtasks_ldif2db(standalone, topology_be_latest.logcap.log, args)
+ # Assert none.
+ assert topology_be_latest.logcap.contains("ldif2db successful")
diff --git a/src/lib389/lib389/tests/cli/dsrc_test.py
b/src/lib389/lib389/tests/cli/dsrc_test.py
new file mode 100644
index 0000000..6e2155c
--- /dev/null
+++ b/src/lib389/lib389/tests/cli/dsrc_test.py
@@ -0,0 +1,180 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import pytest
+import logging
+import ldap
+import sys
+
+from lib389.cli_base.dsrc import dsrc_to_ldap
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+if MAJOR >= 3:
+ import configparser
+
+log = logging.getLogger(__name__)
+
+def write_inf(content):
+ with open('/tmp/dsrc_test.inf', 'w') as f:
+ f.write(content)
+
+(a)pytest.mark.skipif(sys.version_info < (3,0), reason="requires python3")
+def test_dsrc_single_section():
+ # Write out inf.
+ write_inf("""
+[localhost]
+ """)
+ # Parse it and assert the content
+ with pytest.raises(configparser.NoOptionError):
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i == {
+ 'uri': 'ldaps://localhost:636',
+ 'basedn': None,
+ 'binddn': None,
+ 'saslmech': None,
+ 'tls_cacertdir': None,
+ 'tls_cert': None,
+ 'tls_key': None,
+ 'tls_reqcert': ldap.OPT_X_TLS_HARD,
+ 'starttls': False}
+ )
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+basedn = dc=example,dc=com
+binddn = cn=Directory Manager
+starttls = true
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i == {
+ 'uri': 'ldaps://localhost:636',
+ 'basedn': 'dc=example,dc=com',
+ 'binddn': 'cn=Directory Manager',
+ 'saslmech': None,
+ 'tls_cacertdir': None,
+ 'tls_cert': None,
+ 'tls_key': None,
+ 'tls_reqcert': ldap.OPT_X_TLS_HARD,
+ 'starttls': True}
+ )
+
+(a)pytest.mark.skipif(sys.version_info < (3,0), reason="requires python3")
+def test_dsrc_two_section():
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+
+[localhost2]
+uri = ldaps://localhost:6362
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i == {
+ 'uri': 'ldaps://localhost:636',
+ 'basedn': None,
+ 'binddn': None,
+ 'saslmech': None,
+ 'tls_cacertdir': None,
+ 'tls_cert': None,
+ 'tls_key': None,
+ 'tls_reqcert': ldap.OPT_X_TLS_HARD,
+ 'starttls': False}
+ )
+
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost2', log)
+ assert(i == {
+ 'uri': 'ldaps://localhost:6362',
+ 'basedn': None,
+ 'binddn': None,
+ 'saslmech': None,
+ 'tls_cacertdir': None,
+ 'tls_cert': None,
+ 'tls_key': None,
+ 'tls_reqcert': ldap.OPT_X_TLS_HARD,
+ 'starttls': False}
+ )
+
+ # Section doesn't exist
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost3', log)
+ assert(i is None)
+
+(a)pytest.mark.skipif(sys.version_info < (3,0), reason="requires python3")
+def test_dsrc_reqcert():
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+tls_reqcert = invalid
+ """)
+ # Parse it and assert the content
+ with pytest.raises(Exception):
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+tls_reqcert = hard
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i['tls_reqcert'] == ldap.OPT_X_TLS_HARD)
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+tls_reqcert = allow
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i['tls_reqcert'] == ldap.OPT_X_TLS_ALLOW)
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+tls_reqcert = never
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i['tls_reqcert'] == ldap.OPT_X_TLS_NEVER)
+
+(a)pytest.mark.skipif(sys.version_info < (3,0), reason="requires python3")
+def test_dsrc_saslmech():
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+saslmech = INVALID_MECH
+ """)
+ # Parse it and assert the content
+ with pytest.raises(Exception):
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+saslmech = EXTERNAL
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i['saslmech'] == 'EXTERNAL')
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+saslmech = PLAIN
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i['saslmech'] == 'PLAIN')
diff --git a/src/lib389/lib389/tests/config.py b/src/lib389/lib389/tests/config.py
new file mode 100644
index 0000000..3b9a472
--- /dev/null
+++ b/src/lib389/lib389/tests/config.py
@@ -0,0 +1,40 @@
+import logging
+import six
+logging.basicConfig(level=logging.DEBUG)
+log = logging.getLogger(__name__)
+
+DN_RMANAGER = 'uid=rmanager,cn=config'
+
+auth = {'host': 'localhost',
+ 'port': 22389,
+ 'binddn': 'cn=directory manager',
+ 'bindpw': 'password'}
+
+
+class MockDirSrv(object):
+ host = 'localhost'
+ port = 22389
+ sslport = 0
+
+ def __str__(self):
+ if self.sslport:
+ return 'ldaps://%s:%s' % (self.host, self.sslport)
+ else:
+ return 'ldap://%s:%s' % (self.host, self.port)
+
+
+def expect(entry, name, value):
+ assert entry, "Bad entry %r " % entry
+ assert entry.getValue(name) == value, \
+ ("Bad value for entry %s. Expected %r vs %r" %
+ (entry, entry.getValue(name), value))
+
+
+def entry_equals(e1, e2):
+ """compare using str()"""
+ return str(e1) == str(e2)
+
+
+def dfilter(my_dict, keys):
+ """Filter a dict in a 2.4-compatible way"""
+ return dict([(k, v) for k, v in six.iteritems(my_dict) if k in keys])
diff --git a/src/lib389/lib389/tests/config_compare_test.py
b/src/lib389/lib389/tests/config_compare_test.py
new file mode 100644
index 0000000..f0c45cd
--- /dev/null
+++ b/src/lib389/lib389/tests/config_compare_test.py
@@ -0,0 +1,56 @@
+import os
+import sys
+import time
+import ldap
+import logging
+import pytest
+from lib389._constants import *
+from lib389.properties import *
+from lib389.tasks import *
+from lib389.utils import *
+
+from lib389.idm.group import Groups
+from lib389.idm.user import UserAccounts, UserAccount
+
+from lib389.topologies import topology_st, topology_i2
+from lib389.config import Config
+
+DEBUGGING = os.getenv('DEBUGGING', False)
+
+if DEBUGGING is not False:
+ DEBUGGING = True
+
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+
+log = logging.getLogger(__name__)
+
+
+def test_config_compare(topology_i2):
+ """
+ Compare test between cn=config of two different Directory Server intance.
+ """
+ if DEBUGGING:
+ # Add debugging steps(if any)...
+ pass
+
+ st1_config = topology_i2.ins.get('standalone1').config
+ st2_config = topology_i2.ins.get('standalone2').config
+ # 'nsslapd-port' attribute is expected to be same in cn=config comparison,
+ # but they are different in our testing environment
+ # as we are using 2 DS instances running, both running simultaneuosly.
+ # Hence explicitly adding 'nsslapd-port' to compare_exclude.
+ st1_config._compare_exclude.append('nsslapd-port')
+ st2_config._compare_exclude.append('nsslapd-port')
+
+ assert(Config.compare(st1_config, st2_config) == True)
+ log.info("Test PASSED")
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/config_test.py
b/src/lib389/lib389/tests/config_test.py
new file mode 100644
index 0000000..7966dab
--- /dev/null
+++ b/src/lib389/lib389/tests/config_test.py
@@ -0,0 +1,87 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""Brooker classes to organize ldap methods.
+ Stuff is split in classes, like:
+ * Replica
+ * Backend
+ * Suffix
+
+ You will access this from:
+ DirSrv.backend.methodName()
+"""
+
+from . import config
+from .config import *
+
+import lib389
+from lib389 import DirSrv
+
+
+conn = None
+added_entries = None
+added_backends = None
+
+MOCK_REPLICA_ID = '12'
+MOCK_TESTREPLICA_DN = "cn=testReplica,cn=ldbm database,cn=plugins,cn=config"
+
+
+def setup():
+ # uses an existing 389 instance
+ # add a suffix
+ # add an agreement
+ # This setup is quite verbose but to test DirSrv method we should
+ # do things manually. A better solution would be to use an LDIF.
+ global conn
+ conn = DirSrv(**config.auth)
+ conn.verbose = True
+ conn.added_entries = []
+ conn.added_backends = set(['o=mockbe1'])
+ conn.added_replicas = []
+ """
+ # add a backend for testing ruv and agreements
+ addbackend_harn(conn, 'testReplica')
+
+ # add another backend for testing replica.add()
+ addbackend_harn(conn, 'testReplicaCreation')
+ """
+
+
+def teardown():
+ global conn
+ conn.config.loglevel([lib389.LOG_CACHE])
+ conn.config.loglevel([256], service='access')
+
+ """
+ drop_added_entries(conn)
+ conn.delete_s(','.join(['cn="o=testreplica"',
DN_MAPPING_TREE]))
+ drop_backend(conn, 'o=testreplica')
+ #conn.delete_s('o=testreplica')
+ """
+
+
+def loglevel_test():
+ vals = [lib389.LOG_CACHE, lib389.LOG_REPLICA, lib389.LOG_CONNECT]
+ expected = sum(vals)
+ assert conn.config.loglevel(vals) == expected
+ ret = conn.config.get('nsslapd-errorlog-level')
+ assert ret == str(expected), "expected: %r got: %r" % (expected, ret)
+
+
+def loglevel_update_test():
+ vals = [lib389.LOG_CACHE, lib389.LOG_CONNECT]
+ e = sum(vals)
+ assert conn.config.loglevel(vals) == e
+ vals = [lib389.LOG_REPLICA]
+ ret = conn.config.loglevel(vals, update=True)
+ assert ret == (e + sum(vals)), "expected %s got %s" % (e + sum(vals), ret)
+
+
+def access_loglevel_test():
+ vals = [lib389.LOG_CACHE, lib389.LOG_REPLICA, lib389.LOG_CONNECT]
+ assert conn.config.loglevel(vals, service='access') == sum(vals)
diff --git a/src/lib389/lib389/tests/configurations/__init__.py
b/src/lib389/lib389/tests/configurations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib389/lib389/tests/configurations/config_001003006_test.py
b/src/lib389/lib389/tests/configurations/config_001003006_test.py
new file mode 100644
index 0000000..300122a
--- /dev/null
+++ b/src/lib389/lib389/tests/configurations/config_001003006_test.py
@@ -0,0 +1,98 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import ldap
+
+import logging
+import sys
+import time
+
+import pytest
+
+from lib389 import DirSrv
+from lib389._constants import *
+from lib389.properties import *
+
+DEBUGGING = os.getenv('DEBUGGING', default=False)
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+log = logging.getLogger(__name__)
+
+### WARNING:
+# We can't use topology here, as we need to force python install!
+
+REQUIRED_DNS = [
+ 'dc=example,dc=com',
+ 'ou=Groups,dc=example,dc=com',
+ 'ou=People,dc=example,dc=com',
+ 'ou=Special Users,dc=example,dc=com',
+ 'cn=Accounting Managers,ou=Groups,dc=example,dc=com',
+ 'cn=HR Managers,ou=Groups,dc=example,dc=com',
+ 'cn=QA Managers,ou=Groups,dc=example,dc=com',
+ 'cn=PD Managers,ou=Groups,dc=example,dc=com',
+ 'cn=Directory Administrators,dc=example,dc=com',
+]
+
+class TopologyMain(object):
+ def __init__(self, standalones=None, masters=None,
+ consumers=None, hubs=None):
+ if standalones:
+ if isinstance(standalones, dict):
+ self.ins = standalones
+ else:
+ self.standalone = standalones
+ if masters:
+ self.ms = masters
+ if consumers:
+ self.cs = consumers
+ if hubs:
+ self.hs = hubs
+
+(a)pytest.fixture(scope="module")
+def topology_st(request):
+ """Create DS standalone instance"""
+
+ if DEBUGGING:
+ standalone = DirSrv(verbose=True)
+ else:
+ standalone = DirSrv(verbose=False)
+ args_instance[SER_HOST] = HOST_STANDALONE1
+ args_instance[SER_PORT] = PORT_STANDALONE1
+ # args_instance[SER_SECURE_PORT] = SECUREPORT_STANDALONE1
+ args_instance[SER_SERVERID_PROP] = SERVERID_STANDALONE1
+ args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX
+ args_standalone = args_instance.copy()
+ standalone.allocate(args_standalone)
+ instance_standalone = standalone.exists()
+ if instance_standalone:
+ standalone.delete()
+ standalone.create(pyinstall=True, version='001003006')
+ standalone.open()
+
+ def fin():
+ if DEBUGGING:
+ standalone.stop()
+ else:
+ standalone.delete()
+
+ request.addfinalizer(fin)
+
+ return TopologyMain(standalones=standalone)
+
+def test_install_sample_entries(topology_st):
+ # Assert that our entries match.
+
+ entries = topology_st.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE)
+
+ for entry in entries:
+ assert(entry.dn in REQUIRED_DNS)
+ # We can make this assert the full object content, plugins and more later.
+
+ assert(True)
diff --git a/src/lib389/lib389/tests/conftest.py b/src/lib389/lib389/tests/conftest.py
new file mode 100644
index 0000000..97156fd
--- /dev/null
+++ b/src/lib389/lib389/tests/conftest.py
@@ -0,0 +1,121 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import string
+import os
+import sys
+import pytest
+from tempfile import mkdtemp
+from shutil import rmtree
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+
+def ldrop(numchar, inputstr):
+ """
+ Drop numchar characters from the beginning of each line in inputstring.
+ """
+ if MAJOR >= 3:
+ return "\n".join(map(lambda x: x[numchar:],
inputstr.split("\n")))
+ else:
+ return "\n".join([x[numchar:] for x in string.split(inputstr,
"\n")])
+
+
+(a)pytest.fixture
+def fake_ds_class():
+ """
+ Returns fake DSInstance-like class
+ """
+ class FakeDSInstance(object):
+ def return_one(self):
+ return 1
+
+ return FakeDSInstance
+
+
+(a)pytest.fixture(scope="module")
+def fake_ds_modules(request):
+ """
+ Returns path to directory containing fake modules.
+ """
+
+ # module_dir contains fake ds modules, that can be used for testing
+ module_dir = mkdtemp()
+ old_cwd = os.getcwd()
+
+ # when fixture is destroyed, we change back to old cwd
+ # and delete fake module dir
+ def fin():
+ os.chdir(old_cwd)
+ rmtree(module_dir)
+ request.addfinalizer(fin)
+
+ # make module_dir a python package
+ with open(module_dir + "/__init__.py", 'a'):
+ os.utime(module_dir + "/__init__.py", None)
+
+ # create module module_dir/repl.py with few functions
+ with open(module_dir + "/repl.py", "w") as repl_file:
+ repl_module = """
+ def with_no_arg(ds):
+ return ds
+
+ def with_one_arg(ds, arg):
+ return ds, arg
+
+ def return_argument_given(ds, arg):
+ return arg
+
+ def with_kwarg(ds, kwarg=2):
+ return kwarg
+
+ def with_all(ds, arg1, arg2, kwarg1=1, kwarg2=2):
+ return arg1, arg2, kwarg1, kwarg2
+
+ """
+ # Drop 12 chars from left and write to file
+ repl_file.write(ldrop(12, repl_module))
+
+ os.mkdir(module_dir + "/plugin")
+
+ # make module_dir/plugin a python package
+ with open(module_dir + "/plugin/__init__.py", 'a'):
+ os.utime(module_dir + "/plugin/__init__.py", None)
+
+ # create module module_dir/plugin/automember.py with few functions
+ with open(module_dir + "/plugin/automember.py", "w") as
automem_file:
+ automember_module = """
+ def with_no_arg(ds):
+ return ds
+
+ def with_one_arg(ds, arg):
+ return ds, arg
+
+ def calling_ds_method(ds, arg):
+ return (ds, ds.return_one(), arg)
+
+ def calling_repl(ds, arg1, arg2):
+ return ds.repl.return_argument_given(1) + arg1 + arg2
+
+ def calling_repl_with_kwarg2(ds):
+ return ds.repl.with_kwarg()
+
+ def calling_repl_with_kwarg1(ds):
+ return ds.repl.with_kwarg(kwarg=1)
+
+ def with_all(ds, arg1, arg2, kwarg1=1, kwarg2=2):
+ a, b, c, d = ds.repl.with_all(1, 4, kwarg1=3)
+ return a + arg1, b + arg2, c + kwarg1, d + kwarg2
+
+ """
+ # Drop 12 chars from left and write to file
+ automem_file.write(ldrop(12, automember_module))
+
+ os.chdir(os.path.dirname(module_dir))
+ sys.path.append(os.path.dirname(module_dir))
+ return module_dir
diff --git a/src/lib389/lib389/tests/dereference_test.py
b/src/lib389/lib389/tests/dereference_test.py
new file mode 100644
index 0000000..1336736
--- /dev/null
+++ b/src/lib389/lib389/tests/dereference_test.py
@@ -0,0 +1,99 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+from lib389._constants import *
+from lib389 import DirSrv, Entry
+import ldap
+import pytest
+import logging
+
+logging.getLogger(__name__).setLevel(logging.DEBUG)
+log = logging.getLogger(__name__)
+
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+
+
+class TopologyStandalone(object):
+ def __init__(self, standalone):
+ standalone.open()
+ self.standalone = standalone
+
+
+(a)pytest.fixture(scope='module')
+def topology(request):
+ standalone = DirSrv(verbose=False)
+ standalone.log.debug('Instance allocated')
+ args = {SER_HOST: LOCALHOST,
+ SER_PORT: INSTANCE_PORT,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ standalone.allocate(args)
+ if standalone.exists():
+ standalone.delete()
+ standalone.create()
+ standalone.open()
+
+ def fin():
+ standalone.delete()
+ request.addfinalizer(fin)
+
+ return TopologyStandalone(standalone)
+
+
+(a)pytest.fixture(scope='module')
+def user(topology):
+ """Create user entries"""
+
+ for i in range(0, 2):
+ uentry = Entry('uid=test%s,%s' % (i, DEFAULT_SUFFIX))
+ uentry.setValues('objectclass', 'top',
'extensibleobject')
+ uentry.setValues('uid', 'test')
+ topology.standalone.add_s(uentry)
+
+
+(a)pytest.fixture(scope='module')
+def group(topology):
+ """Create a group entry"""
+
+ gentry = Entry('cn=testgroup,%s' % DEFAULT_SUFFIX)
+ gentry.setValues('objectclass', 'top', 'extensibleobject')
+ gentry.setValues('cn', 'testgroup')
+ for i in range(0, 2):
+ gentry.setValues('uniqueMember', 'uid=test%s,%s' % (i,
DEFAULT_SUFFIX))
+ topology.standalone.add_s(gentry)
+
+
+def test_dereference(topology, user, group):
+ """Test dereference search argument formating
+ Check, that result is correct
+ """
+
+ log.info('Check, that our deref Control Value is correctly formatted')
+ with pytest.raises(ldap.UNAVAILABLE_CRITICAL_EXTENSION):
+ result, control_response = topology.standalone.dereference(
+ 'uniqueMember:dn,uid;uniqueMember:dn,uid',
+ filterstr='(cn=testgroup)')
+
+ result, control_response = topology.standalone.dereference(
+ 'uniqueMember:cn,uid,objectclass', filterstr='(cn=testgroup)')
+
+ log.info('Check, that the dereference search result is correct')
+ assert result[0][2][0].entry == [{'derefVal':
+ 'uid=test1,dc=example,dc=com',
+ 'derefAttr': 'uniqueMember',
+ 'attrVals': [{'vals':
+ ['top',
+ 'extensibleobject'],
+ 'type':
'objectclass'},
+ {'vals': ['test',
'test1'],
+ 'type': 'uid'}]}]
+
+
+if __name__ == '__main__':
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main('-s -v %s' % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/dirsrv_log_test.py
b/src/lib389/lib389/tests/dirsrv_log_test.py
new file mode 100644
index 0000000..920e67a
--- /dev/null
+++ b/src/lib389/lib389/tests/dirsrv_log_test.py
@@ -0,0 +1,123 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+from lib389._constants import *
+from lib389.utils import ensure_bytes, ensure_str
+from lib389 import DirSrv, Entry
+import pytest
+import time
+import shutil
+import datetime
+from dateutil.tz import tzoffset
+
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+
+
+class TopologyStandalone(object):
+ def __init__(self, standalone):
+ standalone.open()
+ self.standalone = standalone
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ standalone = DirSrv(verbose=False)
+ standalone.log.debug("Instance allocated")
+ args = {
+ SER_PORT: INSTANCE_PORT,
+ # SER_DEPLOYED_DIR: INSTANCE_PREFIX,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ standalone.allocate(args)
+ if standalone.exists():
+ standalone.delete()
+ standalone.create()
+ standalone.open()
+ standalone.config.set('nsslapd-accesslog-logbuffering', 'off')
+
+ def fin():
+ standalone.delete()
+ request.addfinalizer(fin)
+ # We have to wait for time to elapse for the access log to be flushed.
+
+ return TopologyStandalone(standalone)
+
+
+def test_access_log_rotation(topology):
+ """
+ Check we can parse rotated logs as well as active log.
+ """
+
+ # Artificially rotate the log.
+ lpath = topology.standalone.ds_access_log._get_log_path()
+ shutil.copyfile(lpath, lpath + ensure_bytes('.20160515-104822'))
+ # check we have the right number of lines.
+ access_lines = topology.standalone.ds_access_log.readlines_archive()
+ assert(len(access_lines) > 0)
+ access_lines = topology.standalone.ds_access_log.match_archive('.*fd=.*')
+ assert(len(access_lines) > 0)
+
+
+def test_access_log(topology):
+ """Check the parsing of the access log"""
+ access_lines = topology.standalone.ds_access_log.readlines()
+ assert(len(access_lines) > 0)
+ access_lines = topology.standalone.ds_access_log.match('.*fd=.*')
+ assert(len(access_lines) > 0)
+ # Test the line parser in a basic way.
+ assert(
+ topology.standalone.ds_access_log.parse_line('[27/Apr/2016:12:49:49.726093186
+1000] conn=1 fd=64 slot=64 connection from ::1 to ::1') ==
+ {
+ 'slot': '64', 'remote': '::1',
'action': 'CONNECT', 'timestamp':
'[27/Apr/2016:12:49:49.726093186 +1000]', 'fd': '64',
'conn': '1', 'local': '::1',
+ 'datetime': datetime.datetime(2016, 4, 27, 12, 0, 0, 726093,
tzinfo=tzoffset(None, 36000))
+ }
+ )
+ assert(
+ topology.standalone.ds_access_log.parse_line('[27/Apr/2016:12:49:49.727235997
+1000] conn=1 op=2 SRCH base="cn=config" scope=0
filter="(objectClass=*)" attrs="nsslapd-instancedir nsslapd-errorlog
nsslapd-accesslog nsslapd-auditlog nsslapd-certdir nsslapd-schemadir nsslapd-bakdir
nsslapd-ldifdir"') == # noqa
+ {
+ 'rem': 'base="cn=config" scope=0
filter="(objectClass=*)" attrs="nsslapd-instancedir nsslapd-errorlog
nsslapd-accesslog nsslapd-auditlog nsslapd-certdir nsslapd-schemadir nsslapd-bakdir
nsslapd-ldifdir"', # noqa
+ 'action': 'SRCH', 'timestamp':
'[27/Apr/2016:12:49:49.727235997 +1000]', 'conn': '1',
'op': '2',
+ 'datetime': datetime.datetime(2016, 4, 27, 12, 0, 0, 727235,
tzinfo=tzoffset(None, 36000))
+ }
+ )
+ assert(
+ topology.standalone.ds_access_log.parse_line('[27/Apr/2016:12:49:49.736297002
+1000] conn=1 op=4 fd=64 closed - U1') ==
+ {
+ 'status': 'U1', 'fd': '64', 'action':
'DISCONNECT', 'timestamp': '[27/Apr/2016:12:49:49.736297002
+1000]', 'conn': '1', 'op': '4',
+ 'datetime': datetime.datetime(2016, 4, 27, 12, 0, 0, 736297,
tzinfo=tzoffset(None, 36000))
+ }
+ )
+ assert(
+ topology.standalone.ds_access_log.parse_line('[27/Apr/2016:12:49:49.736297002
-1000] conn=1 op=4 fd=64 closed - U1') ==
+ {
+ 'status': 'U1', 'fd': '64', 'action':
'DISCONNECT', 'timestamp': '[27/Apr/2016:12:49:49.736297002
-1000]', 'conn': '1', 'op': '4',
+ 'datetime': datetime.datetime(2016, 4, 27, 12, 0, 0, 736297,
tzinfo=tzoffset(None, -36000))
+ }
+ )
+
+
+def test_error_log(topology):
+ """Check the parsing of the error log"""
+ # No need to sleep, it's not buffered.
+ error_lines = topology.standalone.ds_error_log.readlines()
+ assert(len(error_lines) > 0)
+ error_lines = topology.standalone.ds_error_log.match('.*started.*')
+ assert(len(error_lines) > 0)
+
+ assert(
+ topology.standalone.ds_error_log.parse_line('[27/Apr/2016:13:46:35.775670167
+1000] slapd started. Listening on All Interfaces port 54321 for LDAP requests') ==
# noqa
+ {
+ 'timestamp': '[27/Apr/2016:13:46:35.775670167 +1000]',
'message': 'slapd started. Listening on All Interfaces port 54321 for LDAP
requests',
+ 'datetime': datetime.datetime(2016, 4, 27, 13, 0, 0, 775670,
tzinfo=tzoffset(None, 36000))
+ }
+ )
+
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -vv %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/dirsrv_test.py
b/src/lib389/lib389/tests/dirsrv_test.py
new file mode 100644
index 0000000..f118369
--- /dev/null
+++ b/src/lib389/lib389/tests/dirsrv_test.py
@@ -0,0 +1,215 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import os
+import pwd
+import logging
+import pytest
+from lib389._constants import *
+from lib389.properties import *
+from lib389 import DirSrv, Entry
+
+logging.getLogger(__name__).setLevel(logging.DEBUG)
+log = logging.getLogger(__name__)
+
+TEST_DN = "uid=test,%s" % DEFAULT_SUFFIX
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+INSTANCE_BACKUP = os.environ.get('BACKUPDIR', DEFAULT_BACKUPDIR)
+
+
+class TopologyInstance(object):
+ def __init__(self, instance):
+ self.instance = instance
+
+
+(a)pytest.fixture(scope='module')
+def topology(request):
+ instance = DirSrv(verbose=False)
+
+ args = {SER_HOST: LOCALHOST,
+ SER_PORT: INSTANCE_PORT,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ instance.allocate(args)
+
+ if instance.exists():
+ instance.delete()
+
+ def fin():
+ if instance.exists():
+ instance.delete()
+ request.addfinalizer(fin)
+
+ return TopologyInstance(instance)
+
+
+def _add_user(topology):
+ """Add a user to the instance"""
+
+ topology.instance.add_s(Entry((TEST_DN, {'objectclass':
+ "top person".split(),
+ 'objectclass':
+ "organizationalPerson",
+ 'objectclass':
"inetOrgPerson",
+ 'uid': 'test',
+ 'sn': 'test',
+ 'cn': 'test'})))
+ log.info('User %s was added' % TEST_DN)
+
+
+def test_allocate(topology):
+ """Test instance.allocate() function
+ for correct state and arguments
+ """
+
+ log.info('Check instance state is allocated')
+ assert topology.instance.state == DIRSRV_STATE_ALLOCATED
+
+ log.info('Try to add user. Failure is excpected')
+ with pytest.raises(Exception):
+ _add_user(topology)
+
+ if os.getuid() == 0:
+ userid = DEFAULT_USER
+ else:
+ userid = pwd.getpwuid(os.getuid())[0]
+
+ log.info('Verify the settings')
+ assert topology.instance.host == LOCALHOST
+ assert topology.instance.port == INSTANCE_PORT
+ assert topology.instance.sslport is None
+ assert topology.instance.binddn == DN_DM
+ assert topology.instance.bindpw == PW_DM
+ assert topology.instance.creation_suffix == DEFAULT_SUFFIX
+ assert topology.instance.userid == userid
+ assert topology.instance.serverid == INSTANCE_SERVERID
+ assert topology.instance.groupid == topology.instance.userid
+ assert topology.instance.backupdir == INSTANCE_BACKUP
+
+ log.info('Check that we can change the settings of an allocated DirSrv')
+ args = {SER_SERVERID_PROP: INSTANCE_SERVERID,
+ SER_HOST: LOCALHOST,
+ SER_PORT: INSTANCE_PORT,
+ SER_ROOT_DN: "uid=foo"}
+ topology.instance.allocate(args)
+
+ log.info('Check instance state is allocated')
+ assert topology.instance.state == DIRSRV_STATE_ALLOCATED
+
+ log.info('Verify the settings')
+ assert topology.instance.host == LOCALHOST
+ assert topology.instance.port == INSTANCE_PORT
+ assert topology.instance.sslport is None
+ assert topology.instance.binddn == "uid=foo"
+ assert topology.instance.bindpw == PW_DM
+ assert topology.instance.creation_suffix == DEFAULT_SUFFIX
+ assert topology.instance.userid == userid
+ assert topology.instance.serverid == INSTANCE_SERVERID
+ assert topology.instance.groupid == topology.instance.userid
+ assert topology.instance.backupdir == INSTANCE_BACKUP
+
+ log.info('Restore back the valid parameters and check')
+ args = {SER_SERVERID_PROP: INSTANCE_SERVERID,
+ SER_HOST: LOCALHOST,
+ SER_PORT: INSTANCE_PORT}
+ topology.instance.allocate(args)
+
+ log.info('Check instance state is allocated')
+ assert topology.instance.state == DIRSRV_STATE_ALLOCATED
+
+ log.info('Verify the settings')
+ assert topology.instance.host == LOCALHOST
+ assert topology.instance.port == INSTANCE_PORT
+ assert topology.instance.sslport is None
+ assert topology.instance.binddn == DN_DM
+ assert topology.instance.bindpw == PW_DM
+ assert topology.instance.creation_suffix == DEFAULT_SUFFIX
+ assert topology.instance.userid == userid
+ assert topology.instance.serverid == INSTANCE_SERVERID
+ assert topology.instance.groupid == topology.instance.userid
+ assert topology.instance.backupdir == INSTANCE_BACKUP
+
+
+def test_create(topology):
+ """Test instance creation
+ Check status before and after
+ Try to add a user
+ """
+
+ log.info('Check instance state is allocated')
+ assert topology.instance.state == DIRSRV_STATE_ALLOCATED
+
+ topology.instance.create()
+
+ log.info('Check instance state is offline')
+ assert topology.instance.state == DIRSRV_STATE_OFFLINE
+
+ log.info('Try to add user. Failure is excpected')
+ with pytest.raises(Exception):
+ _add_user(topology)
+
+
+def test_open(topology):
+ """Test instance opening
+ Check status before and after
+ Try to add a user
+ """
+
+ log.info('Check instance state is offline')
+ assert topology.instance.state == DIRSRV_STATE_OFFLINE
+
+ topology.instance.open()
+
+ log.info('Check instance state is online')
+ assert topology.instance.state == DIRSRV_STATE_ONLINE
+
+ log.info('Try to add user. Success is excpected')
+ _add_user(topology)
+
+
+def test_close(topology):
+ """Test instance closing
+ Check status before and after
+ Try to add a user
+ """
+
+ log.info('Check instance state is online')
+ assert topology.instance.state == DIRSRV_STATE_ONLINE
+
+ topology.instance.close()
+
+ log.info('Check instance state is offline')
+ assert topology.instance.state == DIRSRV_STATE_OFFLINE
+
+ log.info('Try to add user. Failure is excpected')
+ with pytest.raises(Exception):
+ _add_user(topology)
+
+
+def test_delete(topology):
+ """Test instance deletion
+ Check status before and after
+ Try to add a user
+ """
+
+ log.info('Check instance state is offline')
+ assert topology.instance.state == DIRSRV_STATE_OFFLINE
+
+ topology.instance.delete()
+
+ log.info('Check instance state is allocated')
+ assert topology.instance.state == DIRSRV_STATE_ALLOCATED
+
+ log.info('Try to add user. Failure is excpected')
+ with pytest.raises(Exception):
+ _add_user(topology)
+
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main('-s -v %s' % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/dsadmin_basic_test.py
b/src/lib389/lib389/tests/dsadmin_basic_test.py
new file mode 100644
index 0000000..8164c84
--- /dev/null
+++ b/src/lib389/lib389/tests/dsadmin_basic_test.py
@@ -0,0 +1,126 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+""" Testing basic functionalities of DirSrv
+
+
+"""
+import ldap
+import lib389
+from lib389 import DirSrv
+from lib389 import NoSuchEntryError
+from ldap import *
+# from nose.tools import *
+from . import config
+from .config import *
+
+conn = None
+added_entries = None
+
+
+def setup():
+ global conn
+ try:
+ conn = DirSrv(**config.auth)
+ conn.verbose = True
+ conn.added_entries = []
+ except SERVER_DOWN as e:
+ log.error("To run tests you need a working 389 instance %s" %
+ config.auth)
+ raise e
+
+
+def tearDown():
+ global conn
+
+ # reduce log level
+ conn.config.loglevel([lib389.LOG_CACHE])
+ conn.config.loglevel([256], service='access')
+
+ for e in conn.added_entries:
+ try:
+ conn.delete_s(e)
+ except ldap.NO_SUCH_OBJECT:
+ log.warn("entry not found %r" % e)
+
+
+def bind_test():
+ print ("conn: %s" % conn)
+
+
+def setupBindDN_UID_test():
+ user = {
+ 'binddn': 'uid=rmanager1,cn=config',
+ 'bindpw': 'password'
+ }
+ e = conn.setupBindDN(**user)
+ conn.added_entries.append(e.dn)
+
+ assert e.dn == user['binddn'], "Bad entry: %r " % e
+ expected = conn.getEntry(user['binddn'], ldap.SCOPE_BASE)
+ assert entry_equals(
+ e, expected), "Mismatching entry %r vs %r" % (e, expected)
+
+
+def setupBindDN_CN_test():
+ user = {
+ 'binddn': 'cn=rmanager1,cn=config',
+ 'bindpw': 'password'
+ }
+ e = conn.setupBindDN(**user)
+ conn.added_entries.append(e.dn)
+ assert e.dn == user['binddn'], "Bad entry: %r " % e
+ expected = conn.getEntry(user['binddn'], ldap.SCOPE_BASE)
+ assert entry_equals(
+ e, expected), "Mismatching entry %r vs %r" % (e, expected)
+
+
+def setupChangelog_default_test():
+ e = conn.replica.changelog()
+ conn.added_entries.append(e.dn)
+ assert e.dn, "Bad changelog entry: %r " % e
+ assert e.getValue('nsslapd-changelogdir').endswith("changelogdb"),
\
+ "Mismatching entry %r " % e.data.get('nsslapd-changelogdir')
+ conn.delete_s("cn=changelog5,cn=config")
+
+
+def setupChangelog_test():
+ e = conn.replica.changelog(dbname="mockChangelogDb")
+ conn.added_entries.append(e.dn)
+ assert e.dn, "Bad changelog entry: %r " % e
+ assert
e.getValue('nsslapd-changelogdir').endswith("mockChangelogDb"), \
+ "Mismatching entry %r " % e.data.get('nsslapd-changelogdir')
+ conn.delete_s("cn=changelog5,cn=config")
+
+
+def setupChangelog_full_test():
+ e = conn.replica.changelog(dbname="/tmp/mockChangelogDb")
+ conn.added_entries.append(e.dn)
+
+ assert e.dn, "Bad changelog entry: %r " % e
+ expect(e, 'nsslapd-changelogdir', "/tmp/mockChangelogDb")
+ conn.delete_s("cn=changelog5,cn=config")
+
+
+# @raises(NoSuchEntryError)
+def getMTEntry_missing_test():
+ e = conn.getMTEntry('o=MISSING')
+ return e
+
+
+def getMTEntry_present_test():
+ suffix = 'o=addressbook16'
+ bename = 'addressbookd16db'
+ backendEntry, dummy = conn.backend.add(suffix, benamebase=bename)
+ suffixEntry = conn.backend.setup_mt(suffix, bename)
+
+ e = conn.getMTEntry(suffix)
+
+ conn.backend.delete(bename)
+ conn.delete_s(suffixEntry.dn)
+ assert e, "Entry should be present %s" % suffix
diff --git a/src/lib389/lib389/tests/dsadmin_create_remove_test.py
b/src/lib389/lib389/tests/dsadmin_create_remove_test.py
new file mode 100644
index 0000000..1c2233e
--- /dev/null
+++ b/src/lib389/lib389/tests/dsadmin_create_remove_test.py
@@ -0,0 +1,97 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""
+Test creation and deletion of instances
+"""
+import ldap
+import os
+import socket
+from lib389 import DirSrv, DN_CONFIG
+from lib389.tools import DirSrvTools
+# from nose import *
+
+added_instances = []
+
+
+def setup():
+ global added_instances
+
+
+def teardown():
+ global added_instances
+ for instance in added_instances:
+ cmd = "/usr/bin/sudo remove-ds.pl -i slapd-%s" % instance
+ try:
+ os.system(cmd)
+ except:
+ log.exception("error executing %r" % cmd)
+
+
+def default_test():
+ host = socket.gethostname()
+ port = 10200
+ binddn = "cn=directory manager"
+ bindpw = "password"
+ suffix = 'dc=example,dc=com'
+ basedn = DN_CONFIG
+ scope = ldap.SCOPE_BASE
+ filt = "(objectclass=*)"
+ instance_name = ['m1', 'm2']
+
+ instance_config = {
+ 'cfgdshost': host,
+ 'cfgdsport': port,
+ 'cfgdsuser': 'admin',
+ 'cfgdspwd': 'admin',
+ 'newrootpw': 'password',
+ 'newhost': host,
+ 'newport': port,
+ 'newinstance': instance_name[0],
+ 'newsuffix': suffix,
+ 'setup_admin': True,
+ }
+ try:
+ m1 = DirSrv(host, port, binddn, bindpw)
+ except:
+ m1 = DirSrvTools.createInstance(instance_config, verbose=0)
+ added_instances.append(instance_config['newinstance'])
+
+# filename = "%s/slapd-%s/ldif/Example.ldif" % (m1.sroot, m1.inst)
+# m1.importLDIF(filename, "dc=example,dc=com", None, True)
+# m1.exportLDIF('/tmp/ldif', "dc=example,dc=com", False, True)
+ print (m1.sroot, m1.inst, m1.errlog)
+ ent = m1.getEntry(basedn, scope, filt, None)
+ if ent:
+ print (ent.passwordmaxage)
+ instance_config.update({
+ 'newinstance': instance_name[1],
+ 'newport': port + 10,
+
+ })
+ m1 = DirSrvTools.createInstance(instance_config, verbose=0)
+ added_instances.append(instance_config['newinstance'])
+ # m1.stop(True)
+ # m1.start(True)
+ suffix = "dc=example2,dc=com"
+ bename = "example2db"
+ backendEntry, dummy = m1.backend.add(suffix, bename)
+ suffixEntry = m1.backend.setup_mt(suffix, bename)
+ cn = backendEntry.getValue('cn')
+ print (cn)
+ entry = m1.getEntry(DN_CONFIG, ldap.SCOPE_SUBTREE, "(cn=" + cn +
")")
+ print ("new backend entry is:")
+ print (entry)
+ print (entry.getValues('objectclass'))
+ print (entry.OBJECTCLASS)
+ results = m1.search_s("cn=monitor", ldap.SCOPE_SUBTREE)
+ print (results)
+ results = m1.getBackendsForSuffix("dc=example,dc=com")
+ print (results)
+
+ print ("done")
diff --git a/src/lib389/lib389/tests/dsadmin_test.py
b/src/lib389/lib389/tests/dsadmin_test.py
new file mode 100644
index 0000000..114cb9d
--- /dev/null
+++ b/src/lib389/lib389/tests/dsadmin_test.py
@@ -0,0 +1,263 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+# from nose import *
+# from nose.tools import *
+
+from . import config
+from .config import log
+from .config import *
+
+import ldap
+import time
+import lib389
+from lib389 import DirSrv, Entry
+from lib389 import utils
+from lib389.tools import DirSrvTools
+from subprocess import Popen
+
+
+conn = None
+added_entries = None
+added_backends = None
+
+
+def harn_nolog():
+ conn.config.loglevel([lib389.LOG_DEFAULT])
+ conn.config.loglevel([lib389.LOG_DEFAULT], service='access')
+
+
+def setup():
+ global conn
+ conn = DirSrv(**config.auth)
+ conn.verbose = True
+ conn.added_entries = []
+ conn.added_backends = set(['o=mockbe2'])
+ conn.added_replicas = []
+ harn_nolog()
+
+
+def setup_backend():
+ global conn
+ suffix = 'o=addressbook6'
+ backend = 'addressbook6db'
+
+ # create backend and suffix
+ backendEntry, dummy = conn.backend.add(suffix, benamebase=backend)
+ suffixEntry = conn.backend.setup_mt(suffix, backend)
+
+
+def teardown():
+ global conn
+ conn.rebind()
+ drop_added_entries(conn)
+
+
+def drop_added_entries(conn):
+ while conn.added_entries:
+ try:
+ e = conn.added_entries.pop()
+ log.info("removing entries %r" % conn.added_backends)
+ conn.delete_s(e)
+ except ldap.NOT_ALLOWED_ON_NONLEAF:
+ log.error("Entry is not a leaf: %r" % e)
+ except ldap.NO_SUCH_OBJECT:
+ log.error("Cannot remove entry: %r" % e)
+
+ log.info("removing backends %r" % conn.added_backends)
+ for suffix in conn.added_backends:
+ try:
+ drop_backend(conn, suffix)
+ except:
+ log.exception("error removing %r" % suffix)
+ for r in conn.added_replicas:
+ try:
+ drop_backend(conn, suffix=None, bename=r)
+ except:
+ log.exception("error removing %r" % r)
+
+
+def drop_backend(conn, suffix, bename=None, maxnum=50):
+ if not bename:
+ bename = [x.dn for x in conn.getBackendsForSuffix(suffix)]
+
+ if not bename:
+ return None
+
+ assert bename, "Missing bename for %r" % suffix
+ if not hasattr(bename, '__iter__'):
+ bename = [','.join(['cn=%s' % bename, lib389.DN_LDBM])]
+ for be in bename:
+ log.debug("removing entry from %r" % be)
+ leaves = [x.dn for x in conn.search_s(
+ be, ldap.SCOPE_SUBTREE, '(objectclass=*)', ['cn'])]
+ # start deleting the leaves - which have the max number of ","
+ leaves.sort(key=lambda x: x.count(","))
+ while leaves and maxnum:
+ # to avoid infinite loops
+ # limit the iterations
+ maxnum -= 1
+ try:
+ log.debug("removing %s" % leaves[-1])
+ conn.delete_s(leaves[-1])
+ leaves.pop()
+ except:
+ leaves.insert(0, leaves.pop())
+
+ if not maxnum:
+ raise Exception("BAD")
+
+
+#
+# Tests
+#
+
+
+def addbackend_harn(conn, name, beattrs=None):
+ """Create the suffix o=name and its backend."""
+ suffix = "o=%s" % name
+ e = Entry((suffix, {
+ 'objectclass': ['top', 'organization'],
+ 'o': [name]
+ }))
+
+ try:
+ ret = conn.addSuffix(suffix, bename=name, beattrs=beattrs)
+ except ldap.ALREADY_EXISTS:
+ raise
+ finally:
+ conn.added_backends.add(suffix)
+
+ conn.add(e)
+ conn.added_entries.append(e.dn)
+
+ return ret
+
+
+def setupBackend_ok_test():
+ "setupBackend_ok calls brooker.Backend.add"
+ try:
+ backendEntry, dummy = conn.backend.add('o=mockbe5',
+ benamebase='mockbe5')
+ assert backendEntry
+ except ldap.ALREADY_EXISTS:
+ raise
+ finally:
+ conn.backend.delete(benamebase='mockbe5')
+
+
+# @raises(ldap.ALREADY_EXISTS)
+def setupBackend_double_test():
+ "setupBackend_double calls brooker.Backend.add"
+ backendEntry, dummy = conn.backend.add('o=mockbe3',
benamebase='mockbe3')
+ backendEntry, dummy = conn.backend.add('o=mockbe3',
benamebase='mockbe3')
+
+
+def addsuffix_test():
+ # identical to getMTEntry_present_test in dsadmin_basic_test
+ # addbackend_harn(conn, 'addressbook16')
+ # conn.added_backends.add('o=addressbook16')
+ pass
+
+
+def addreplica_write_test():
+ suffix = 'o=ab3'
+ backend = 'ab3'
+ user = {
+ 'binddn': 'uid=rmanager,cn=config',
+ 'bindpw': 'password'
+ }
+ replica = {
+ 'suffix': suffix,
+ 'type': lib389.MASTER_TYPE,
+ 'id': 124
+ }
+ replica.update(user)
+
+ # create backend and suffix
+ backendEntry, dummy = conn.backend.add(suffix, benamebase=backend)
+ suffixEntry = conn.backend.setup_mt(suffix, backend)
+
+ ret = conn.replicaSetupAll(replica)
+
+ assert ret != -1, "Error in setup replica: %s" % ret
+
+
+def prepare_master_replica_test():
+ """prepare_master_replica -> Replica.changelog"""
+ user = {
+ 'binddn': 'uid=rmanager,cn=config',
+ 'bindpw': 'password'
+ }
+ conn.enableReplLogging()
+ e = conn.setupBindDN(**user)
+ conn.added_entries.append(e.dn)
+
+ # only for Writable
+ e = conn.replica.changelog()
+ conn.added_entries.append(e.dn)
+
+
+# @with_setup(setup_backend)
+def setupAgreement_test():
+ consumer = MockDirSrv()
+ args = {
+ 'suffix': "o=addressbook6",
+ # 'bename': "userRoot",
+ 'binddn': "uid=rmanager,cn=config",
+ 'bindpw': "password",
+ 'rtype': lib389.MASTER_TYPE,
+ 'rid': '1234'
+ }
+ conn.replica.add(**args)
+ conn.added_entries.append(args['binddn'])
+
+ dn_replica = conn.createAgreement(consumer, args)
+
+
+def stop_start_test():
+ # dunno why DirSrv.start|stop writes to dirsrv error-log
+ conn.errlog = "/tmp/dirsrv-errlog"
+ open(conn.errlog, "w").close()
+ DirSrvTools.stop(conn)
+ log.info("server stopped")
+ DirSrvTools.start(conn)
+ log.info("server start")
+ time.sleep(5)
+ # save and restore conn settings after restart
+ tmp = conn.added_backends, conn.added_entries
+ setup()
+ conn.added_backends, conn.added_entries = tmp
+ assert conn.search_s(
+ *utils.searches['NAMINGCONTEXTS']), "Missing namingcontexts"
+
+
+def setupSSL_test():
+ ssl_args = {
+ 'dirsrv': conn,
+ 'secport': 22636,
+ 'sourcedir': None,
+ 'secargs': {'nsSSLPersonalitySSL': 'localhost'},
+ }
+ cert_dir = conn.getDseAttr('nsslapd-certdir')
+ assert cert_dir, "Cannot retrieve cert dir"
+
+ log.info("Initialize the cert store with an empty password: %r", cert_dir)
+ fd_null = open('/dev/null', 'w')
+ open('%s/pin.txt' % cert_dir, 'w').close()
+ cmd_initialize = 'certutil -d %s -N -f %s/pin.txt' % (cert_dir, cert_dir)
+ Popen(cmd_initialize.split(), stderr=fd_null)
+
+ log.info("Creating a self-signed cert for the server in %r" % cert_dir)
+ cmd_mkcert = ('certutil -d %s -S -n localhost -t CTu,Cu,Cu '
+ '-s cn=localhost -x' % cert_dir)
+ Popen(cmd_mkcert.split(), stdin=open("/dev/urandom"), stderr=fd_null)
+
+ log.info("Testing ssl configuration")
+ ssl_args.update({'dirsrv': conn})
+ DirSrvTools.setupSSL(**ssl_args)
diff --git a/src/lib389/lib389/tests/dseldif_test.py
b/src/lib389/lib389/tests/dseldif_test.py
new file mode 100644
index 0000000..44eb4d4
--- /dev/null
+++ b/src/lib389/lib389/tests/dseldif_test.py
@@ -0,0 +1,130 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import logging
+import pytest
+
+from lib389._constants import *
+from lib389.dseldif import DSEldif
+from lib389.topologies import topology_st as topo
+
+DEBUGGING = os.getenv('DEBUGGING', False)
+
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+
+log = logging.getLogger(__name__)
+
+
+(a)pytest.mark.parametrize("entry_dn", (DN_CONFIG,
+ DN_CONFIG_LDBM))
+def test_get_singlevalue(topo, entry_dn):
+ """Check that we can get an attribute value under different
suffixes"""
+
+ dse_ldif = DSEldif(topo.standalone)
+
+ log.info("Get 'cn' attr from {}".format(entry_dn))
+ attr_values = dse_ldif.get(entry_dn, "cn")
+ assert attr_values == ["config"]
+
+ log.info("Get 'nonexistent' attr from {}".format(entry_dn))
+ attr_values = dse_ldif.get(entry_dn, "nonexistent")
+ assert not attr_values
+
+
+def test_get_multivalue(topo):
+ """Check that we can get attribute values"""
+
+ dse_ldif = DSEldif(topo.standalone)
+
+ log.info("Get objectClass from {}".format(DN_CONFIG))
+ attr_values = dse_ldif.get(DN_CONFIG, "objectClass")
+ assert len(attr_values) == 3
+ assert "top" in attr_values
+ assert "extensibleObject" in attr_values
+ assert "nsslapdConfig" in attr_values
+
+
+(a)pytest.mark.parametrize("fake_attr_value", ("fake value",
+ "fakevalue"))
+def test_add(topo, fake_attr_value):
+ """Check that we can add an attribute to a given
suffix"""
+
+ dse_ldif = DSEldif(topo.standalone)
+ fake_attr = "fakeAttr"
+
+ log.info("Add {} to {}".format(fake_attr, DN_CONFIG))
+ dse_ldif.add(DN_CONFIG, fake_attr, fake_attr_value)
+ attr_values = dse_ldif.get(DN_CONFIG, fake_attr)
+ assert attr_values == [fake_attr_value]
+
+ log.info("Clean up")
+ dse_ldif.delete(DN_CONFIG, fake_attr)
+ assert not dse_ldif.get(DN_CONFIG, fake_attr)
+
+
+def test_replace(topo):
+ """Check that we can replace an attribute to a given
suffix"""
+
+ dse_ldif = DSEldif(topo.standalone)
+ port_attr = "nsslapd-port"
+ port_value = "390"
+
+ log.info("Get default value of {}".format(port_attr))
+ default_value = dse_ldif.get(DN_CONFIG, port_attr)[0]
+
+ log.info("Replace {} with {}".format(port_attr, port_value))
+ dse_ldif.replace(DN_CONFIG, port_attr, port_value)
+ attr_values = dse_ldif.get(DN_CONFIG, port_attr)
+ assert attr_values == [port_value]
+
+ log.info("Restore default value")
+ dse_ldif.replace(DN_CONFIG, port_attr, default_value)
+
+
+def test_delete_singlevalue(topo):
+ """Check that we can delete an attribute from a given
suffix"""
+
+ dse_ldif = DSEldif(topo.standalone)
+ fake_attr = "fakeAttr"
+ fake_attr_values = ["fake1", "fake2", "fake3"]
+
+ log.info("Add multivalued {} to {}".format(fake_attr, DN_CONFIG))
+ for value in fake_attr_values:
+ dse_ldif.add(DN_CONFIG, fake_attr, value)
+
+ log.info("Delete {}".format(fake_attr_values[0]))
+ dse_ldif.delete(DN_CONFIG, fake_attr, fake_attr_values[0])
+ attr_values = dse_ldif.get(DN_CONFIG, fake_attr)
+ assert len(attr_values) == 2
+ assert fake_attr_values[0] not in attr_values
+ assert fake_attr_values[1] in attr_values
+ assert fake_attr_values[2] in attr_values
+
+ log.info("Clean up")
+ dse_ldif.delete(DN_CONFIG, fake_attr)
+ assert not dse_ldif.get(DN_CONFIG, fake_attr)
+
+
+def test_delete_multivalue(topo):
+ """Check that we can delete attributes from a given
suffix"""
+
+ dse_ldif = DSEldif(topo.standalone)
+ fake_attr = "fakeAttr"
+ fake_attr_values = ["fake1", "fake2", "fake3"]
+
+ log.info("Add multivalued {} to {}".format(fake_attr, DN_CONFIG))
+ for value in fake_attr_values:
+ dse_ldif.add(DN_CONFIG, fake_attr, value)
+
+ log.info("Delete all values of {}".format(fake_attr))
+ dse_ldif.delete(DN_CONFIG, fake_attr)
+ assert not dse_ldif.get(DN_CONFIG, fake_attr)
+
diff --git a/src/lib389/lib389/tests/effective_rights_test.py
b/src/lib389/lib389/tests/effective_rights_test.py
new file mode 100644
index 0000000..333d70a
--- /dev/null
+++ b/src/lib389/lib389/tests/effective_rights_test.py
@@ -0,0 +1,93 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+from lib389._constants import *
+from lib389 import DirSrv, Entry
+import pytest
+import logging
+
+logging.getLogger(__name__).setLevel(logging.DEBUG)
+log = logging.getLogger(__name__)
+
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+TEST_USER = 'uid=test,%s' % DEFAULT_SUFFIX
+TEST_GROUP = 'cn=testgroup,%s' % DEFAULT_SUFFIX
+
+
+class TopologyInstance(object):
+ def __init__(self, instance):
+ instance.open()
+ self.instance = instance
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ instance = DirSrv(verbose=False)
+ instance.log.debug("Instance allocated")
+ args = {SER_HOST: LOCALHOST,
+ SER_PORT: INSTANCE_PORT,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ instance.allocate(args)
+ if instance.exists():
+ instance.delete()
+ instance.create()
+ instance.open()
+
+ def fin():
+ if instance.exists():
+ instance.delete()
+ request.addfinalizer(fin)
+
+ return TopologyInstance(instance)
+
+
+(a)pytest.fixture(scope="module")
+def add_user(topology):
+ """Create a user entry"""
+
+ log.info('Create a user entry: %s' % TEST_USER)
+ uentry = Entry(TEST_USER)
+ uentry.setValues('objectclass', 'top', 'extensibleobject')
+ uentry.setValues('uid', 'test')
+ topology.instance.add_s(uentry)
+ # topology.instance.log.debug("Created user entry as:" ,uentry.dn)
+
+
+(a)pytest.fixture(scope="module")
+def add_group(topology):
+ """Create a group for the user to have some rights
to"""
+
+ log.info('Create a group entry: %s' % TEST_GROUP)
+ gentry = Entry(TEST_GROUP)
+ gentry.setValues('objectclass', 'top', 'extensibleobject')
+ gentry.setValues('cn', 'testgroup')
+ topology.instance.add_s(gentry)
+
+
+def test_effective_rights(topology, add_user, add_group):
+ """Run an effective rights search
+ and compare actual results with expected
+ """
+
+ log.info('Search for effective rights with get_effective_rights() '
+ 'function')
+ result = topology.instance.get_effective_rights(TEST_USER,
+ filterstr='(cn=testgroup)',
+ attrlist=['cn'])
+
+ rights = result[0]
+ log.info('Assert that "attributeLevelRights: cn:rsc" and '
+ '"entryLevelRights: v"')
+ assert rights.getValue('attributeLevelRights') == 'cn:rsc'
+ assert rights.getValue('entryLevelRights') == 'v'
+
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/entry_test.py
b/src/lib389/lib389/tests/entry_test.py
new file mode 100644
index 0000000..46fee29
--- /dev/null
+++ b/src/lib389/lib389/tests/entry_test.py
@@ -0,0 +1,98 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import os
+from lib389 import Entry
+import lib389
+import pytest
+
+import logging
+logging.basicConfig(level=logging.DEBUG)
+log = logging.getLogger(__name__)
+
+
+class TestEntry(object):
+ """A properly initialized Entry:
+ - accepts well-formed or empty dn and tuples;
+ - refuses empty dn
+ """
+ def test_init_empty(self):
+ e = Entry('')
+ assert not e.dn
+
+ def test_init_with_str(self):
+ e = Entry('o=pippo')
+ assert e.dn == 'o=pippo'
+
+ def test_init_badstr(self):
+ # This should not be allowed
+ with pytest.raises(ValueError):
+ e = Entry('no equal sign here')
+
+ def test_init_with_tuple(self):
+ expected = 'pippo'
+ given = 'o=pippo'
+ t = (given, {
+ 'o': [expected],
+ 'objectclass': ['organization', 'top']
+ })
+ e = Entry(t)
+ assert e.dn == given
+ assert expected in e.o
+
+ def test_update(self):
+ expected = 'pluto minnie'
+ given = {'cn': expected}
+ t = ('o=pippo', {
+ 'o': ['pippo'],
+ 'objectclass': ['organization', 'top']
+ })
+
+ e = Entry(t)
+ e.update(given)
+ assert e.cn == expected, "Bad cn: %s, expected: %s" % (e.cn, expected)
+
+ def test_update_complex(self):
+ # compare two entries created with different methods
+ nsuffix, replid, replicatype = ("dc=example,dc=com", 5,
+ lib389.REPLICA_RDWR_TYPE)
+ binddnlist, legacy = ['uid=pippo, cn=config'], 'off'
+ dn = "dc=example,dc=com"
+ entry = Entry(dn)
+ entry.setValues(
+ 'objectclass', "top", "nsds5replica",
"extensibleobject")
+ entry.setValues('cn', "replica")
+ entry.setValues('nsds5replicaroot', nsuffix)
+ entry.setValues('nsds5replicaid', str(replid))
+ entry.setValues('nsds5replicatype', str(replicatype))
+ entry.setValues('nsds5flags', "1")
+ entry.setValues('nsds5replicabinddn', binddnlist)
+ entry.setValues('nsds5replicalegacyconsumer', legacy)
+
+ uentry = Entry((
+ dn, {'objectclass': ["top", "nsds5replica",
"extensibleobject"],
+ 'cn': ["replica"]})
+ )
+ log.debug("Entry created with dict:", uentry)
+ # Entry.update *replaces*, so be careful with multi-valued attrs
+ uentry.update({
+ 'nsds5replicaroot': nsuffix,
+ 'nsds5replicaid': str(replid),
+ 'nsds5replicatype': str(replicatype),
+ 'nsds5flags': '1',
+ 'nsds5replicabinddn': binddnlist,
+ 'nsds5replicalegacyconsumer': legacy
+ })
+ uentry_s, entry_s = list(map(str, (uentry, entry)))
+ assert uentry_s == entry_s, "Mismatching entries [%r] vs [%r]" % (
+ uentry, entry)
+
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/healthcheck_test.py
b/src/lib389/lib389/tests/healthcheck_test.py
new file mode 100644
index 0000000..a36e6fb
--- /dev/null
+++ b/src/lib389/lib389/tests/healthcheck_test.py
@@ -0,0 +1,61 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+
+import pytest
+import ldap
+
+from lib389.topologies import topology_st
+from lib389.plugins import ReferentialIntegrityPlugin
+
+from lib389.lint import *
+
+def test_hc_backend_mt(topology_st):
+ mt = topology_st.standalone.mappingtrees.get('userRoot')
+ # We have to remove the MT from a backend.
+ mt.delete()
+ be = topology_st.standalone.backends.get('userRoot')
+ result = be._lint_mappingtree()
+ # We have to check this by name, not object, as the result changes
+ # the affected dn in the result.
+ assert result['dsle'] == 'DSBLE0001'
+
+def test_hc_encryption(topology_st):
+ topology_st.standalone.encryption.set('sslVersionMin', 'TLS1.0')
+ result = topology_st.standalone.encryption._lint_check_tls_version()
+ assert result == DSELE0001
+
+def test_hc_config(topology_st):
+ # Check the HR timestamp
+ topology_st.standalone.config.set('nsslapd-logging-hr-timestamps-enabled',
'off')
+ result = topology_st.standalone.config._lint_hr_timestamp()
+ assert result == DSCLE0001
+
+ # Check the password scheme check.
+ topology_st.standalone.config.set('passwordStorageScheme', 'SSHA')
+ result = topology_st.standalone.config._lint_passwordscheme()
+ assert result == DSCLE0002
+
+def test_hc_referint(topology_st):
+ plugin = ReferentialIntegrityPlugin(topology_st.standalone)
+ plugin.enable()
+
+ # Assert we don't get an error when delay is 0.
+ plugin.set('referint-update-delay', '0')
+ result = plugin._lint_update_delay()
+ assert result is None
+
+ # Assert we get an error when delay is not 0.
+ plugin.set('referint-update-delay', '10')
+ result = plugin._lint_update_delay()
+ assert result == DSRILE0001
+
+ # Assert we don't get an error when plugin is disabled.
+ plugin.disable()
+ result = plugin._lint_update_delay()
+ assert result is None
diff --git a/src/lib389/lib389/tests/idm/__init__.py
b/src/lib389/lib389/tests/idm/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib389/lib389/tests/idm/account_test.py
b/src/lib389/lib389/tests/idm/account_test.py
new file mode 100644
index 0000000..d78db03
--- /dev/null
+++ b/src/lib389/lib389/tests/idm/account_test.py
@@ -0,0 +1,70 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+
+
+import os
+import logging
+import pytest
+import ldap
+
+from lib389.idm.user import UserAccounts
+from lib389.topologies import topology_st as topology
+from lib389._constants import DEFAULT_SUFFIX
+
+DEBUGGING = os.getenv('DEBUGGING', False)
+
+if DEBUGGING is not False:
+ DEBUGGING = True
+
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+
+log = logging.getLogger(__name__)
+
+
+def test_account_locking(topology):
+ """
+ Ensure that user and group management works as expected.
+ """
+ if DEBUGGING:
+ # Add debugging steps(if any)...
+ pass
+
+ users = UserAccounts(topology.standalone, DEFAULT_SUFFIX)
+
+ user_properties = {
+ 'uid': 'testuser',
+ 'cn' : 'testuser',
+ 'sn' : 'user',
+ 'uidNumber' : '1000',
+ 'gidNumber' : '2000',
+ 'homeDirectory' : '/home/testuser',
+ 'userPassword' : 'password'
+ }
+ testuser = users.create(properties=user_properties)
+
+ assert(testuser.is_locked() is False)
+ testuser.lock()
+ assert(testuser.is_locked() is True)
+
+ # Check a bind fails
+ with pytest.raises(ldap.UNWILLING_TO_PERFORM):
+ conn = testuser.bind('password')
+ conn.unbind_s()
+
+ testuser.unlock()
+ assert(testuser.is_locked() is False)
+
+ # Check the bind works.
+ conn = testuser.bind('password')
+ conn.unbind_s()
+
+ log.info('Test PASSED')
diff --git a/src/lib389/lib389/tests/idm/services_test.py
b/src/lib389/lib389/tests/idm/services_test.py
new file mode 100644
index 0000000..6b6c457
--- /dev/null
+++ b/src/lib389/lib389/tests/idm/services_test.py
@@ -0,0 +1,55 @@
+import os
+import sys
+import time
+import ldap
+import logging
+import pytest
+from lib389 import DirSrv, Entry, tools, tasks
+from lib389.tools import DirSrvTools
+from lib389._constants import *
+from lib389.properties import *
+from lib389.tasks import *
+from lib389.utils import *
+
+from lib389.idm.organisationalunit import OrganisationalUnits
+from lib389.idm.services import ServiceAccounts
+
+from lib389.topologies import topology_st as topology
+
+DEBUGGING = os.getenv('DEBUGGING', False)
+
+if DEBUGGING is not False:
+ DEBUGGING = True
+
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+
+log = logging.getLogger(__name__)
+
+def test_services(topology):
+ """
+ Test and assert that a simple service account can be bound to and created.
+
+ These are really useful in simple tests.
+ """
+ ous = OrganisationalUnits(topology.standalone, DEFAULT_SUFFIX)
+ services = ServiceAccounts(topology.standalone, DEFAULT_SUFFIX)
+
+ # Create the OU for them.
+ ous.create(properties={
+ 'ou': 'Services',
+ 'description': 'Computer Service accounts which request DS
bind',
+ })
+ # Now, we can create the services from here.
+ service = services.create(properties={
+ 'cn': 'testbind',
+ 'userPassword': 'Password1'
+ })
+
+ conn = service.bind('Password1')
+ conn.unbind_s()
+
+
+
diff --git a/src/lib389/lib389/tests/idm/user_and_group_test.py
b/src/lib389/lib389/tests/idm/user_and_group_test.py
new file mode 100644
index 0000000..09ff74e
--- /dev/null
+++ b/src/lib389/lib389/tests/idm/user_and_group_test.py
@@ -0,0 +1,93 @@
+import os
+import sys
+import time
+import ldap
+import logging
+import pytest
+from lib389 import DirSrv, Entry, tools, tasks
+from lib389.tools import DirSrvTools
+from lib389._constants import *
+from lib389.properties import *
+from lib389.tasks import *
+from lib389.utils import *
+
+from lib389.idm.user import UserAccounts, TEST_USER_PROPERTIES
+from lib389.idm.group import Groups
+
+from lib389.topologies import topology_st as topology
+
+DEBUGGING = os.getenv('DEBUGGING', False)
+
+if DEBUGGING is not False:
+ DEBUGGING = True
+
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+
+log = logging.getLogger(__name__)
+
+
+def test_users_and_groups(topology):
+ """
+ Ensure that user and group management works as expected.
+ """
+ if DEBUGGING:
+ # Add debugging steps(if any)...
+ pass
+
+ users = UserAccounts(topology.standalone, DEFAULT_SUFFIX)
+ groups = Groups(topology.standalone, DEFAULT_SUFFIX)
+
+ # No user should exist
+
+ assert(len(users.list()) == 0)
+
+ # Create a user
+
+ users.create(properties=TEST_USER_PROPERTIES)
+
+ assert(len(users.list()) == 1)
+
+ testuser = users.get('testuser')
+
+ # Set password
+ testuser.set('userPassword', 'password')
+ # bind
+
+ conn = testuser.bind('password')
+ conn.unbind_s()
+
+ # create group
+ group_properties = {
+ 'cn' : 'group1',
+ 'description' : 'testgroup'
+ }
+
+ group = groups.create(properties=group_properties)
+
+ # user shouldn't be a member
+ assert(not group.is_member(testuser.dn))
+
+ # add user as member
+ group.add_member(testuser.dn)
+ # check they are a member
+ assert(group.is_member(testuser.dn))
+ # remove user from group
+ group.remove_member(testuser.dn)
+ # check they are not a member
+ assert(not group.is_member(testuser.dn))
+
+ group.delete()
+ testuser.delete()
+
+ log.info('Test PASSED')
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
+
diff --git a/src/lib389/lib389/tests/idm/user_compare_i2_test.py
b/src/lib389/lib389/tests/idm/user_compare_i2_test.py
new file mode 100644
index 0000000..c0c2004
--- /dev/null
+++ b/src/lib389/lib389/tests/idm/user_compare_i2_test.py
@@ -0,0 +1,67 @@
+import os
+import sys
+import time
+import ldap
+import logging
+import pytest
+from lib389._constants import *
+from lib389.properties import *
+from lib389.tasks import *
+from lib389.utils import *
+
+from lib389.idm.group import Groups
+from lib389.idm.user import UserAccounts, UserAccount
+
+from lib389.topologies import topology_i2
+
+DEBUGGING = os.getenv('DEBUGGING', False)
+
+if DEBUGGING is not False:
+ DEBUGGING = True
+
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+
+log = logging.getLogger(__name__)
+
+
+def test_user_compare_i2(topology_i2):
+ """
+ Compare test between users of two different Directory Server intances.
+
+ """
+ if DEBUGGING:
+ # Add debugging steps(if any)...
+ pass
+
+ st1_users = UserAccounts(topology_i2.ins.get('standalone1'), DEFAULT_SUFFIX)
+ st2_users = UserAccounts(topology_i2.ins.get('standalone2'), DEFAULT_SUFFIX)
+
+ # Create user
+ user_properties = {
+ 'uid': 'testuser',
+ 'cn': 'testuser',
+ 'sn': 'user',
+ 'uidNumber': '1000',
+ 'gidNumber': '2000',
+ 'homeDirectory': '/home/testuser'
+ }
+
+ st1_users.create(properties=user_properties)
+ st1_testuser = st1_users.get('testuser')
+
+ st2_users.create(properties=user_properties)
+ st2_testuser = st2_users.get('testuser')
+
+ assert(UserAccount.compare(st1_testuser, st2_testuser) == True)
+
+ log.info("Test PASSED")
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/idm/user_compare_m2Repl_test.py
b/src/lib389/lib389/tests/idm/user_compare_m2Repl_test.py
new file mode 100644
index 0000000..19ac1c9
--- /dev/null
+++ b/src/lib389/lib389/tests/idm/user_compare_m2Repl_test.py
@@ -0,0 +1,83 @@
+import os
+import sys
+import time
+import ldap
+import logging
+import pytest
+import time
+from lib389._constants import *
+from lib389.properties import *
+from lib389.tasks import *
+from lib389.utils import *
+
+from lib389.idm.user import UserAccounts, UserAccount
+
+from lib389.topologies import topology_m2
+
+DEBUGGING = os.getenv('DEBUGGING', False)
+
+if DEBUGGING is not False:
+ DEBUGGING = True
+
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+
+log = logging.getLogger(__name__)
+
+
+def test_user_compare_m2Repl(topology_m2):
+ """
+ User compare test between users of master to master replicaton topology.
+ """
+ if DEBUGGING:
+ # Add debugging steps(if any)...
+ pass
+
+ m1 = topology_m2.ms.get('master1')
+ m2 = topology_m2.ms.get('master2')
+
+ m1_m2_agmtdn = m1.agreement.list(suffix=DEFAULT_SUFFIX)[0].dn
+
+ m1_users = UserAccounts(m1, DEFAULT_SUFFIX)
+ m2_users = UserAccounts(m2, DEFAULT_SUFFIX)
+
+ # Create 1st user
+ user1_properties = {
+ 'uid': 'testuser',
+ 'cn': 'testuser',
+ 'sn': 'user',
+ 'uidNumber': '1000',
+ 'gidNumber': '2000',
+ 'homeDirectory': '/home/testuser'
+ }
+
+ m1_users.create(properties=user1_properties)
+ m1_testuser = m1_users.get('testuser')
+
+ log.info("Waiting for completion of replicaation.....")
+ # wait for replication to complete
+ m1.startReplication(m1_m2_agmtdn)
+
+ m1_ruv = m1.replica.ruv(DEFAULT_SUFFIX)
+ m2_ruv = m2.replica.ruv(DEFAULT_SUFFIX)
+
+ log.debug("m1 ruv : " + str(m1_ruv))
+ log.debug("m2 ruv : " + str(m2_ruv))
+ log.debug("RUV diffs: " + str(m1_ruv.getdiffs(m2_ruv)))
+
+ # ruv comparison, if replication is complete then ruv's should be same
+ assert(m1_ruv == m2_ruv)
+ log.info("Replication completed")
+ m2_testuser = m2_users.get('testuser')
+
+ assert(UserAccount.compare(m1_testuser, m2_testuser) == True)
+ log.info("Test PASSED")
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/idm/user_compare_st_test.py
b/src/lib389/lib389/tests/idm/user_compare_st_test.py
new file mode 100644
index 0000000..900d980
--- /dev/null
+++ b/src/lib389/lib389/tests/idm/user_compare_st_test.py
@@ -0,0 +1,92 @@
+import os
+import sys
+import time
+import ldap
+import logging
+import pytest
+from lib389._constants import *
+from lib389.properties import *
+from lib389.tasks import *
+from lib389.utils import *
+
+from lib389.idm.group import Groups
+from lib389.idm.user import UserAccounts, UserAccount
+
+from lib389.topologies import topology_st as topology
+
+DEBUGGING = os.getenv('DEBUGGING', False)
+
+if DEBUGGING is not False:
+ DEBUGGING = True
+
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+
+log = logging.getLogger(__name__)
+
+
+def test_user_compare(topology):
+ """
+ Testing compare function
+ 1. Testing comparison of two different users.
+ 2. Testing comparison of 'str' object with itself, should raise
'ValueError'.
+ 3. Testing comparison of user with similar user (different object id).
+ 4. Testing comparison of user with group.
+ """
+ if DEBUGGING:
+ # Add debugging steps(if any)...
+ pass
+
+ users = UserAccounts(topology.standalone, DEFAULT_SUFFIX)
+ groups = Groups(topology.standalone, DEFAULT_SUFFIX)
+ # Create 1st user
+ user1_properties = {
+ 'uid': 'testuser1',
+ 'cn': 'testuser1',
+ 'sn': 'user',
+ 'uidNumber': '1000',
+ 'gidNumber': '2000',
+ 'homeDirectory': '/home/testuser1'
+ }
+
+ users.create(properties=user1_properties)
+ testuser1 = users.get('testuser1')
+ # Create 2nd user
+ user2_properties = {
+ 'uid': 'testuser2',
+ 'cn': 'testuser2',
+ 'sn': 'user',
+ 'uidNumber': '1001',
+ 'gidNumber': '2002',
+ 'homeDirectory': '/home/testuser2'
+ }
+
+ users.create(properties=user2_properties)
+ testuser2 = users.get('testuser2')
+ # create group
+ group_properties = {
+ 'cn' : 'group1',
+ 'description' : 'testgroup'
+ }
+
+ testuser1_copy = users.get("testuser1")
+ group = groups.create(properties=group_properties)
+
+ assert(UserAccount.compare(testuser1, testuser2) == False)
+
+ with pytest.raises(ValueError):
+ UserAccount.compare("test_str_object","test_str_object")
+
+ assert(UserAccount.compare(testuser1, testuser1_copy) == True)
+ assert(UserAccount.compare(testuser1, group) == False)
+
+ log.info("Test PASSED")
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/index_test.py
b/src/lib389/lib389/tests/index_test.py
new file mode 100644
index 0000000..58dbf3d
--- /dev/null
+++ b/src/lib389/lib389/tests/index_test.py
@@ -0,0 +1,69 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+
+import pytest
+import ldap
+
+from lib389.topologies import topology_st
+
+from lib389.backend import Backends
+from lib389.index import Indexes
+
+def test_default_index_list(topology_st):
+ indexes = Indexes(topology_st.standalone)
+ # create and delete a default index.
+
+ index = indexes.create(properties={
+ 'cn': 'modifytimestamp',
+ 'nsSystemIndex': 'false',
+ 'nsIndexType': 'eq'
+ })
+ default_index_list = indexes.list()
+ found = False
+ for i in default_index_list:
+ if i.dn.startswith('cn=modifytimestamp'):
+ found = True
+ assert found
+ index.delete()
+
+ default_index_list = indexes.list()
+ found = False
+ for i in default_index_list:
+ if i.dn.startswith('cn=modifytimestamp'):
+ found = True
+ assert not found
+
+def test_backend_index(topology_st):
+ backends = Backends(topology_st.standalone)
+ ur_backend = backends.get('userRoot')
+ ur_indexes = ur_backend.get_indexes()
+
+ index = ur_indexes.create(properties={
+ 'cn': 'modifytimestamp',
+ 'nsSystemIndex': 'false',
+ 'nsIndexType': 'eq'
+ })
+
+ index_list = ur_indexes.list()
+ found = False
+ for i in index_list:
+ if i.dn.startswith('cn=modifytimestamp'):
+ found = True
+ assert found
+ index.delete()
+
+ index_list = ur_indexes.list()
+ found = False
+ for i in index_list:
+ if i.dn.startswith('cn=modifytimestamp'):
+ found = True
+ assert not found
+
+
+
diff --git a/src/lib389/lib389/tests/instance/__init__.py
b/src/lib389/lib389/tests/instance/__init__.py
new file mode 100644
index 0000000..d57ac33
--- /dev/null
+++ b/src/lib389/lib389/tests/instance/__init__.py
@@ -0,0 +1,8 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
diff --git a/src/lib389/lib389/tests/instance/setup_test.py
b/src/lib389/lib389/tests/instance/setup_test.py
new file mode 100644
index 0000000..aa2d13b
--- /dev/null
+++ b/src/lib389/lib389/tests/instance/setup_test.py
@@ -0,0 +1,121 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+
+import sys
+import pytest
+from lib389 import DirSrv
+from lib389.cli_base import LogCapture
+from lib389.instance.setup import SetupDs
+from lib389.instance.remove import remove_ds_instance
+from lib389.instance.options import General2Base, Slapd2Base
+from lib389._constants import *
+
+import tempfile
+
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+
+DEBUGGING = True
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+class TopologyInstance(object):
+ def __init__(self, standalone):
+ # For these tests, we don't want to open the instance.
+ # instance.open()
+ self.standalone = standalone
+
+# Need a teardown to destroy the instance.
+(a)pytest.fixture
+def topology(request):
+ instance = DirSrv(verbose=DEBUGGING)
+ instance.log.debug("Instance allocated")
+ args = {SER_PORT: INSTANCE_PORT,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ instance.allocate(args)
+ if instance.exists():
+ instance.delete()
+
+ def fin():
+ if instance.exists() and not DEBUGGING:
+ instance.delete()
+ request.addfinalizer(fin)
+
+ return TopologyInstance(instance)
+
+def test_setup_ds_minimal_dry(topology):
+ # Create the setupDs
+ lc = LogCapture()
+ # Give it the right types.
+ sds = SetupDs(verbose=DEBUGGING, dryrun=True, log=lc.log)
+
+ # Get the dicts from Type2Base, as though they were from _validate_ds_2_config
+ # IE get the defaults back just from Slapd2Base.collect
+ # Override instance name, root password, port and secure port.
+
+ general_options = General2Base(lc.log)
+ general_options.verify()
+ general = general_options.collect()
+
+ slapd_options = Slapd2Base(lc.log)
+ slapd_options.set('instance_name', INSTANCE_SERVERID)
+ slapd_options.set('port', INSTANCE_PORT)
+ slapd_options.set('root_password', PW_DM)
+ slapd_options.verify()
+ slapd = slapd_options.collect()
+
+ sds.create_from_args(general, slapd, {}, None)
+
+ insts = topology.standalone.list(serverid=INSTANCE_SERVERID)
+ # Assert we did not change the system.
+ assert(len(insts) == 0)
+
+def test_setup_ds_minimal(topology):
+ # Create the setupDs
+ lc = LogCapture()
+ # Give it the right types.
+ sds = SetupDs(verbose=DEBUGGING, dryrun=False, log=lc.log)
+
+ # Get the dicts from Type2Base, as though they were from _validate_ds_2_config
+ # IE get the defaults back just from Slapd2Base.collect
+ # Override instance name, root password, port and secure port.
+
+ general_options = General2Base(lc.log)
+ general_options.verify()
+ general = general_options.collect()
+
+ slapd_options = Slapd2Base(lc.log)
+ slapd_options.set('instance_name', INSTANCE_SERVERID)
+ slapd_options.set('port', INSTANCE_PORT)
+ slapd_options.set('root_password', PW_DM)
+ slapd_options.verify()
+ slapd = slapd_options.collect()
+
+ sds.create_from_args(general, slapd, {}, None)
+ insts = topology.standalone.list(serverid=INSTANCE_SERVERID)
+ # Assert we did change the system.
+ assert(len(insts) == 1)
+ # Make sure we can connect
+ topology.standalone.open()
+ # Make sure we can start stop.
+ topology.standalone.stop()
+ topology.standalone.start()
+ # Okay, actually remove the instance
+ remove_ds_instance(topology.standalone)
+
+
+def test_setup_ds_inf_minimal(topology):
+ if MAJOR < 3:
+ return
+ # Write a template inf
+ # Check it?
+ # Setup the server
+
+ pass
+
diff --git a/src/lib389/lib389/tests/krb5_create_test.py
b/src/lib389/lib389/tests/krb5_create_test.py
new file mode 100644
index 0000000..182d51f
--- /dev/null
+++ b/src/lib389/lib389/tests/krb5_create_test.py
@@ -0,0 +1,147 @@
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+from lib389._constants import *
+from lib389.mit_krb5 import MitKrb5, KrbClient
+from lib389 import DirSrv, Entry
+import pytest
+import logging
+import socket
+import subprocess
+
+import ldap
+import ldap.sasl
+
+logging.getLogger(__name__).setLevel(logging.DEBUG)
+log = logging.getLogger(__name__)
+
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+REALM = "EXAMPLE.COM"
+TEST_USER = 'uid=test,%s' % DEFAULT_SUFFIX
+
+KEYTAB = "/tmp/test.keytab"
+CCACHE = "FILE:/tmp/test.ccache"
+
+
+class TopologyInstance(object):
+ def __init__(self, instance):
+ instance.open()
+ self.instance = instance
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ # Create the realm
+ krb = MitKrb5(realm=REALM)
+ instance = DirSrv(verbose=False)
+ instance.log.debug("Instance allocated")
+ # WARNING: If this test fails it's like a hostname issue!!!
+ args = {SER_HOST: socket.gethostname(),
+ SER_PORT: INSTANCE_PORT,
+ SER_REALM: REALM,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ instance.allocate(args)
+ if instance.exists():
+ instance.delete()
+ # Its likely our realm exists too
+ # Remove the old keytab
+ if os.path.exists(KEYTAB):
+ os.remove(KEYTAB)
+ if krb.check_realm():
+ krb.destroy_realm()
+ # This will automatically create the krb entries
+ krb.create_realm()
+ instance.create()
+ instance.open()
+
+ def fin():
+ if instance.exists():
+ instance.delete()
+ if krb.check_realm():
+ krb.destroy_realm()
+ if os.path.exists(KEYTAB):
+ os.remove(KEYTAB)
+ if os.path.exists(CCACHE):
+ os.remove(CCACHE)
+ request.addfinalizer(fin)
+
+ return TopologyInstance(instance)
+
+
+(a)pytest.fixture(scope="module")
+def add_user(topology):
+ """
+ Create a user entry
+ """
+
+ log.info('Create a user entry: %s' % TEST_USER)
+ uentry = Entry(TEST_USER)
+ uentry.setValues('objectclass', 'top', 'extensibleobject')
+ uentry.setValues('uid', 'test')
+ topology.instance.add_s(uentry)
+ # This doesn't matter that we re-open the realm
+ krb = MitKrb5(realm=REALM)
+ krb.create_principal("test")
+ # We extract the kt so we can kinit from it
+ krb.create_keytab("test", "/tmp/test.keytab")
+
+
+def test_gssapi(topology, add_user):
+ """
+ Check that our bind completese with ldapwhoami correctly mapped from
+ the principal to our test user object.
+ """
+ # Init our local ccache
+ kclient = KrbClient("test@%s" % REALM, KEYTAB, CCACHE)
+ # Probably need to change this to NOT be raw python ldap
+ # conn = ldap.initialize("ldap://%s:%s" % (LOCALHOST, INSTANCE_PORT))
+ conn = ldap.initialize("ldap://%s:%s" % (socket.gethostname(),
INSTANCE_PORT))
+ sasl = ldap.sasl.gssapi("test@%s" % REALM)
+ try:
+ conn.sasl_interactive_bind_s('', sasl)
+ except Exception as e:
+ try:
+ print("%s" % subprocess.check_output(['klist']))
+ except Exception as ex:
+ print("%s" % ex)
+ print("%s" % os.environ)
+ print("IF THIS TEST FAILS ITS LIKELY A HOSTNAME ISSUE")
+ raise e
+ assert(conn.whoami_s() == "dn: uid=test,dc=example,dc=com")
+
+ print("Error case 1. Broken Kerberos uid mapping")
+ uidmapping = 'cn=Kerberos uid mapping,cn=mapping,cn=sasl,cn=config'
+ topology.instance.modify_s(uidmapping, [(ldap.MOD_REPLACE,
'nsSaslMapFilterTemplate', '(cn=\1)')])
+ conn0 = ldap.initialize("ldap://%s:%s" % (socket.gethostname(),
INSTANCE_PORT))
+ try:
+ conn0.sasl_interactive_bind_s('', sasl)
+ except Exception as e:
+ print("Exception (expected): %s" % type(e).__name__)
+ print('Desc ' + str(e['desc']))
+ assert isinstance(e, ldap.INVALID_CREDENTIALS)
+
+ # undo
+ topology.instance.modify_s(uidmapping, [(ldap.MOD_REPLACE,
'nsSaslMapFilterTemplate', '(uid=\1)')])
+
+ print("Error case 2. Delete %s from DS" % TEST_USER)
+ topology.instance.delete_s(TEST_USER)
+ try:
+ conn0.sasl_interactive_bind_s('', sasl)
+ except Exception as e:
+ print("Exception (expected): %s" % type(e).__name__)
+ print('Desc ' + str(e['desc']))
+ assert isinstance(e, ldap.INVALID_CREDENTIALS)
+
+ print("SUCCESS")
+
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/ldclt_test.py
b/src/lib389/lib389/tests/ldclt_test.py
new file mode 100644
index 0000000..4b639ec
--- /dev/null
+++ b/src/lib389/lib389/tests/ldclt_test.py
@@ -0,0 +1,64 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+from lib389._constants import *
+from lib389 import DirSrv, Entry
+import pytest
+import logging
+
+import ldap
+import ldap.sasl
+
+logging.getLogger(__name__).setLevel(logging.DEBUG)
+log = logging.getLogger(__name__)
+
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+
+
+class TopologyInstance(object):
+ def __init__(self, instance):
+ instance.open()
+ self.instance = instance
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ # Create the realm
+ instance = DirSrv(verbose=False)
+ instance.log.debug("Instance allocated")
+ args = {SER_PORT: INSTANCE_PORT,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ instance.allocate(args)
+ if instance.exists():
+ instance.delete()
+ instance.create()
+ instance.open()
+
+ def fin():
+ if instance.exists():
+ instance.delete()
+ request.addfinalizer(fin)
+
+ return TopologyInstance(instance)
+
+
+def test_ldctl(topology):
+ """
+ """
+ # Batch create users
+ topology.instance.ldclt.create_users('ou=People,%s' % DEFAULT_SUFFIX,
max=1999)
+ results = topology.instance.search_s('ou=People,%s' % DEFAULT_SUFFIX,
ldap.SCOPE_SUBTREE, filterstr='(objectClass=posixAccount)',
attrlist=['uid'])
+ assert(len(results) == 1000)
+
+ # Run the load test for a few rounds
+ topology.instance.ldclt.bind_loadtest('ou=People,%s' % DEFAULT_SUFFIX,
max=1999)
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/mapped_object_test.py
b/src/lib389/lib389/tests/mapped_object_test.py
new file mode 100644
index 0000000..89cabde
--- /dev/null
+++ b/src/lib389/lib389/tests/mapped_object_test.py
@@ -0,0 +1,23 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+
+from lib389.topologies import topology_st
+from lib389._mapped_object import DSLdapObject
+from lib389.idm.group import Group
+from lib389._constants import DEFAULT_SUFFIX
+
+
+def test_exists(topology_st):
+ """
+ Assert that exists method returns True when entry exists, else False.
+ """
+ group = Group(topology_st.standalone, dn="cn=MyTestGroup,ou=Groups," +
DEFAULT_SUFFIX)
+ assert not group.exists()
+ group.create(properties={'cn': 'MyTestGroup', 'ou':
'groups'})
+ assert group.exists()
diff --git a/src/lib389/lib389/tests/mappingTreeLegacy_test.py
b/src/lib389/lib389/tests/mappingTreeLegacy_test.py
new file mode 100644
index 0000000..282957d
--- /dev/null
+++ b/src/lib389/lib389/tests/mappingTreeLegacy_test.py
@@ -0,0 +1,271 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import os
+import pytest
+import logging
+from lib389._constants import *
+from lib389.properties import *
+from lib389 import DirSrv, InvalidArgumentError
+
+logging.getLogger(__name__).setLevel(logging.DEBUG)
+log = logging.getLogger(__name__)
+
+TEST_REPL_DN = "uid=test,%s" % DEFAULT_SUFFIX
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+INSTANCE_BACKUP = os.environ.get('BACKUPDIR', DEFAULT_BACKUPDIR)
+NEW_SUFFIX_1 = 'o=test_create'
+NEW_BACKEND_1 = 'test_createdb'
+NEW_CHILDSUFFIX_1 = 'o=child1,o=test_create'
+NEW_CHILDBACKEND_1 = 'test_createchilddb'
+NEW_SUFFIX_2 = 'o=test_bis_create'
+NEW_BACKEND_2 = 'test_bis_createdb'
+NEW_CHILDSUFFIX_2 = 'o=child2,o=test_bis_create'
+NEW_CHILDBACKEND_2 = 'test_bis_createchilddb'
+
+
+class TopologyInstance(object):
+ def __init__(self, instance):
+ instance.open()
+ self.instance = instance
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ instance = DirSrv(verbose=False)
+ instance.log.debug("Instance allocated")
+ args = {SER_HOST: LOCALHOST,
+ SER_PORT: INSTANCE_PORT,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ instance.allocate(args)
+ if instance.exists():
+ instance.delete()
+ instance.create()
+ instance.open()
+
+ def fin():
+ instance.delete()
+ request.addfinalizer(fin)
+
+ return TopologyInstance(instance)
+
+
+def test_legacy_list(topology):
+ """This test list with only no param, suffix,
+ bename, suffix+bename (also with invalid values)
+ """
+
+ ents = topology.instance.mappingtree.list()
+ for ent in ents:
+ log.info('test_list: %r' % ent)
+
+ log.info("get with a dedicated suffix")
+ ents = topology.instance.mappingtree.list(suffix=DEFAULT_SUFFIX)
+ assert len(ents) == 1
+ log.info('test_list(suffix): %r' % ent)
+
+ ents = topology.instance.mappingtree.list(suffix="dc=dummy")
+ assert len(ents) == 0
+
+ log.info("get with a dedicated backend name")
+ ents = topology.instance.mappingtree.list(bename=DEFAULT_BENAME)
+ assert len(ents) == 1
+ log.info('test_list(bename): %r' % ent)
+
+ ents = topology.instance.mappingtree.list(bename="dummy")
+ assert len(ents) == 0
+
+ log.info("check backend is taken first")
+ ents = topology.instance.mappingtree.list(suffix="dc=dummy",
+ bename=DEFAULT_BENAME)
+ assert len(ents) == 1
+ log.info('test_list(suffix, bename): %r' % ent)
+
+
+def test_legacy_create(topology):
+ """This test will create 2 backends/mapping trees,
+ then 2 childs backend/mapping tree
+ """
+
+ log.info("before creating mapping trees, the backends must exists")
+ backendEntry = topology.instance.backend.create(
+ NEW_SUFFIX_1, {BACKEND_NAME: NEW_BACKEND_1})
+ backendEntry = topology.instance.backend.create(
+ NEW_SUFFIX_2, {BACKEND_NAME: NEW_BACKEND_2})
+
+ ents = topology.instance.mappingtree.list()
+ nb_mappingtree = len(ents)
+
+ log.info("create a first additional mapping tree")
+ topology.instance.mappingtree.create(NEW_SUFFIX_1, bename=NEW_BACKEND_1)
+ ents = topology.instance.mappingtree.list()
+ assert len(ents) == (nb_mappingtree + 1)
+
+ log.info("create a second additional mapping tree")
+ topology.instance.mappingtree.create(NEW_SUFFIX_2, bename=NEW_BACKEND_2)
+ ents = topology.instance.mappingtree.list()
+ assert len(ents) == (nb_mappingtree + 2)
+
+ log.info("Creating a mapping tree that already exists => "
+ "it just returns the existing MT")
+ topology.instance.mappingtree.create(NEW_SUFFIX_1, bename=NEW_BACKEND_1)
+ ents = topology.instance.mappingtree.list()
+ assert len(ents) == (nb_mappingtree + 2)
+
+ log.info("Creating a mapping tree that already exists => "
+ "it just returns the existing MT")
+ topology.instance.mappingtree.create(NEW_SUFFIX_2, bename=NEW_BACKEND_2)
+ ents = topology.instance.mappingtree.list()
+ assert len(ents) == (nb_mappingtree + 2)
+
+ log.info("before creating mapping trees, the backends must exists")
+ backendEntry = topology.instance.backend.create(
+ NEW_CHILDSUFFIX_1, {BACKEND_NAME: NEW_CHILDBACKEND_1})
+ backendEntry = topology.instance.backend.create(
+ NEW_CHILDSUFFIX_2, {BACKEND_NAME: NEW_CHILDBACKEND_2})
+
+ log.info("create a third additional mapping tree, that is child "
+ "of NEW_SUFFIX_1")
+ topology.instance.mappingtree.create(
+ NEW_CHILDSUFFIX_1, bename=NEW_CHILDBACKEND_1, parent=NEW_SUFFIX_1)
+ ents = topology.instance.mappingtree.list()
+ assert len(ents) == (nb_mappingtree + 3)
+
+ ents = topology.instance.mappingtree.list(suffix=NEW_CHILDSUFFIX_1)
+ assert len(ents) == 1
+ ent = ents[0]
+ assert ent.hasAttr(MT_PROPNAME_TO_ATTRNAME[MT_PARENT_SUFFIX]) and \
+ (ent.getValue(MT_PROPNAME_TO_ATTRNAME[MT_PARENT_SUFFIX]) ==
+ NEW_SUFFIX_1)
+
+ log.info("create a fourth additional mapping tree, that is child "
+ "of NEW_SUFFIX_2")
+ topology.instance.mappingtree.create(NEW_CHILDSUFFIX_2,
+ bename=NEW_CHILDBACKEND_2,
+ parent=NEW_SUFFIX_2)
+ ents = topology.instance.mappingtree.list()
+ assert len(ents) == (nb_mappingtree + 4)
+
+ ents = topology.instance.mappingtree.list(suffix=NEW_CHILDSUFFIX_2)
+ assert len(ents) == 1
+ ent = ents[0]
+ assert ent.hasAttr(MT_PROPNAME_TO_ATTRNAME[MT_PARENT_SUFFIX]) and \
+ (ent.getValue(MT_PROPNAME_TO_ATTRNAME[MT_PARENT_SUFFIX]) ==
+ NEW_SUFFIX_2)
+
+
+def test_legacy_delete(topology):
+ """Delete the mapping tree and check the remaining number.
+ Delete the sub-suffix first.
+ """
+
+ ents = topology.instance.mappingtree.list()
+ nb_mappingtree = len(ents)
+ deleted = 0
+
+ log.debug("delete MT for suffix " + NEW_CHILDSUFFIX_1)
+ topology.instance.mappingtree.delete(suffix=NEW_CHILDSUFFIX_1)
+ deleted += 1
+ ents = topology.instance.mappingtree.list()
+ assert len(ents) == (nb_mappingtree - deleted)
+
+ log.debug("delete MT with backend " + NEW_CHILDBACKEND_2)
+ topology.instance.mappingtree.delete(bename=NEW_CHILDBACKEND_2)
+ deleted += 1
+ ents = topology.instance.mappingtree.list()
+ assert len(ents) == (nb_mappingtree - deleted)
+
+ log.debug("delete MT for suffix %s and with backend %s" %
+ (NEW_SUFFIX_1, NEW_BACKEND_1))
+ topology.instance.mappingtree.delete(suffix=NEW_SUFFIX_1,
+ bename=NEW_BACKEND_1)
+ deleted += 1
+ ents = topology.instance.mappingtree.list()
+ assert len(ents) == (nb_mappingtree - deleted)
+
+ ents = topology.instance.mappingtree.list(suffix=NEW_SUFFIX_2)
+ assert len(ents) == 1
+ log.debug("delete MT with DN %s (dummy suffix/backend)" % (ents[0].dn))
+ topology.instance.mappingtree.delete(suffix="o=dummy",
bename="foo",
+ name=ents[0].dn)
+
+
+def test_legacy_getProperties(topology):
+ """Create one additional mapping tree
+ and try to get properties from it
+ """
+
+ ents = topology.instance.mappingtree.list()
+ nb_mappingtree = len(ents)
+
+ log.info("create a first additional mapping tree")
+ topology.instance.mappingtree.create(NEW_SUFFIX_1, bename=NEW_BACKEND_1)
+ ents = topology.instance.mappingtree.list()
+ assert len(ents) == (nb_mappingtree + 1)
+
+ log.info("check we can get properties from suffix")
+ prop_ref = topology.instance.mappingtree.getProperties(suffix=NEW_SUFFIX_1)
+ log.info("properties [suffix] %s: %r" % (NEW_SUFFIX_1, prop_ref))
+
+ log.info("check we can get properties from backend name")
+ properties = topology.instance.mappingtree.getProperties(
+ bename=NEW_BACKEND_1)
+ for key in list(properties.keys()):
+ assert prop_ref[key] == properties[key]
+ log.info("properties [backend] %s: %r" % (NEW_SUFFIX_1, properties))
+
+ log.info("check we can get properties from suffix AND backend name")
+ properties = topology.instance.mappingtree.getProperties(
+ suffix=NEW_SUFFIX_1, bename=NEW_BACKEND_1)
+ for key in list(properties.keys()):
+ assert prop_ref[key] == properties[key]
+ log.info("properties [suffix+backend]%s: %r" % (NEW_SUFFIX_1, properties))
+
+ log.info("check we can get properties from MT entry")
+ ents = topology.instance.mappingtree.list(suffix=NEW_SUFFIX_1)
+ assert len(ents) == 1
+ ent = ents[0]
+ properties = topology.instance.mappingtree.getProperties(name=ent.dn)
+ for key in list(properties.keys()):
+ assert prop_ref[key] == properties[key]
+ log.info("properties [MT entry DN] %s: %r" % (NEW_SUFFIX_1, properties))
+
+ log.info("check we can get only one properties")
+ properties = topology.instance.mappingtree.getProperties(
+ name=ent.dn, properties=[MT_STATE])
+ assert len(properties) == 1
+ assert properties[MT_STATE] == prop_ref[MT_STATE]
+
+ with pytest.raises(KeyError):
+ properties = topology.instance.mappingtree.getProperties(
+ name=ent.dn, properties=['dummy'])
+
+
+def test_legacy_toSuffix(topology):
+ """Try to get suffix name from existing mapping
tree"""
+
+ ents = topology.instance.mappingtree.list()
+ log.info("check we can get suffix from a mapping tree by entry")
+ suffix = topology.instance.mappingtree.toSuffix(entry=ents[0])
+ assert len(suffix) > 0
+ log.info("suffix (entry) is %s" % suffix[0])
+
+ log.info("check we can get suffix from a mapping tree by name")
+ suffix = topology.instance.mappingtree.toSuffix(name=ents[0].dn)
+ assert len(suffix) > 0
+ log.info("suffix (dn) is %s" % suffix[0])
+
+ log.info("check we can not get suffix from a mapping tree by nothing")
+ with pytest.raises(InvalidArgumentError):
+ suffix = topology.instance.mappingtree.toSuffix()
+
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/mappingtree_test.py
b/src/lib389/lib389/tests/mappingtree_test.py
new file mode 100644
index 0000000..4ec84bb
--- /dev/null
+++ b/src/lib389/lib389/tests/mappingtree_test.py
@@ -0,0 +1,64 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+from lib389._constants import *
+from lib389 import DirSrv, Entry
+import pytest
+from lib389.utils import ensure_bytes, ensure_str
+
+from lib389.mappingTree import MappingTrees, MappingTree
+
+import sys
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+
+DEBUGGING = True
+
+class TopologyStandalone(object):
+ def __init__(self, standalone):
+ standalone.open()
+ self.standalone = standalone
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ standalone = DirSrv(verbose=DEBUGGING)
+ standalone.log.debug("Instance allocated")
+ args = {SER_HOST: LOCALHOST,
+ SER_PORT: INSTANCE_PORT,
+ # SER_DEPLOYED_DIR: INSTANCE_PREFIX,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ standalone.allocate(args)
+ if standalone.exists():
+ standalone.delete()
+ standalone.create()
+ standalone.open()
+
+ def fin():
+ if not DEBUGGING:
+ standalone.delete()
+ request.addfinalizer(fin)
+
+ return TopologyStandalone(standalone)
+
+def test_mappingtree(topology):
+
+ mts = MappingTrees(topology.standalone)
+ mt = mts.create(properties={
+ 'cn': ["dc=newexample,dc=com",],
+ 'nsslapd-state' : 'backend',
+ 'nsslapd-backend' : 'someRoot',
+ })
+
+ rmt = mts.get('someRoot')
+ rmt.delete()
+
+
+
diff --git a/src/lib389/lib389/tests/nss_ssl_test.py
b/src/lib389/lib389/tests/nss_ssl_test.py
new file mode 100644
index 0000000..e1867b6
--- /dev/null
+++ b/src/lib389/lib389/tests/nss_ssl_test.py
@@ -0,0 +1,74 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+from lib389._constants import *
+from lib389 import DirSrv, Entry
+import pytest
+import os
+import logging
+
+from lib389.topologies import topology_st as topo
+
+DEBUGGING = os.getenv('DEBUGGING', False)
+
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+
+log = logging.getLogger(__name__)
+
+def test_nss(topo):
+ """
+ Build a nss db, create a ca, and check that it is correct.
+ """
+
+ standalone = topo.standalone
+
+ # This is a trick. The nss db that ships with DS is broken fundamentally.
+ # THIS ASSUMES old nss format. SQLite will bite us!
+ for f in ('key3.db', 'cert8.db', 'key4.db',
'cert9.db', 'secmod.db', 'pkcs11.txt'):
+ try:
+ os.remove("%s/%s" % (standalone.confdir, f))
+ except:
+ pass
+
+
+ # Check if the db exists. Should be false.
+ assert(standalone.nss_ssl._db_exists() is False)
+ # Create it. Should work.
+ assert(standalone.nss_ssl.reinit() is True)
+ # Check if the db exists. Should be true
+ assert(standalone.nss_ssl._db_exists() is True)
+
+ # Check if ca exists. Should be false.
+ assert(standalone.nss_ssl._rsa_ca_exists() is False)
+ # Create it. Should work.
+ assert(standalone.nss_ssl.create_rsa_ca() is True)
+ # Check if ca exists. Should be true
+ assert(standalone.nss_ssl._rsa_ca_exists() is True)
+
+ # Check if we have a server cert / key. Should be false.
+ assert(standalone.nss_ssl._rsa_key_and_cert_exists() is False)
+ # Create it. Should work.
+ assert(standalone.nss_ssl.create_rsa_key_and_cert() is True)
+ # Check if server cert and key exist. Should be true.
+ assert(standalone.nss_ssl._rsa_key_and_cert_exists() is True)
+
+ # Check making users certs. They should never conflict
+ for user in ('william', 'noriko', 'mark'):
+ assert(standalone.nss_ssl._rsa_user_exists(user) is False)
+ # Create the user cert
+ assert(standalone.nss_ssl.create_rsa_user(user) is True)
+ # Assert it exists now
+ assert(standalone.nss_ssl._rsa_user_exists(user) is True)
+
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -vv %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/paths_test.py
b/src/lib389/lib389/tests/paths_test.py
new file mode 100644
index 0000000..1f86103
--- /dev/null
+++ b/src/lib389/lib389/tests/paths_test.py
@@ -0,0 +1,36 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+
+from lib389.paths import Paths
+
+# Test that we can retrieve the settings from the paths object
+def test_paths():
+ # Make the paths object.
+ p = Paths()
+ # Get a value!
+ v = p.version
+
+# Test that if we make the path object, and we don't read a path from it
+# the filecache state is False
+def test_path_noread():
+ p = Paths()
+ assert(p._defaults_cached is False)
+ p._read_defaults()
+ assert(p._defaults_cached is True)
+
+def test_path_exception():
+ # Trigger the internal path find with a "bad location" and
+ # make sure that we get the exception
+ p = Paths()
+ try:
+ p._get_defaults_loc(search_paths=[])
+ assert(False)
+ except IOError:
+ assert(True)
+
diff --git a/src/lib389/lib389/tests/plugin_test.py
b/src/lib389/lib389/tests/plugin_test.py
new file mode 100644
index 0000000..ca10faf
--- /dev/null
+++ b/src/lib389/lib389/tests/plugin_test.py
@@ -0,0 +1,137 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import os
+import sys
+import time
+import ldap
+import logging
+import pytest
+from lib389 import DirSrv, Entry, tools, tasks
+from lib389.tools import DirSrvTools
+from lib389._constants import *
+from lib389.properties import *
+from lib389.tasks import *
+from lib389.utils import *
+
+from lib389.plugins import *
+
+DEBUGGING = False
+
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+
+
+log = logging.getLogger(__name__)
+
+
+class TopologyStandalone(object):
+ """The DS Topology Class"""
+ def __init__(self, standalone):
+ """Init"""
+ standalone.open()
+ self.standalone = standalone
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ """Create DS Deployment"""
+
+ # Creating standalone instance ...
+ if DEBUGGING:
+ standalone = DirSrv(verbose=True)
+ else:
+ standalone = DirSrv(verbose=False)
+ args_instance[SER_HOST] = HOST_STANDALONE
+ args_instance[SER_PORT] = PORT_STANDALONE
+ args_instance[SER_SERVERID_PROP] = SERVERID_STANDALONE
+ args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX
+ args_standalone = args_instance.copy()
+ standalone.allocate(args_standalone)
+ instance_standalone = standalone.exists()
+ if instance_standalone:
+ standalone.delete()
+ standalone.create()
+ standalone.open()
+
+
+ def fin():
+ """If we are debugging just stop the instances, otherwise remove
+ them
+ """
+ if DEBUGGING:
+ standalone.stop(60)
+ else:
+ standalone.delete()
+
+ request.addfinalizer(fin)
+
+ # Clear out the tmp dir
+ standalone.clearTmpDir(__file__)
+
+ return TopologyStandalone(standalone)
+
+
+def test_plugin_management(topology):
+ """
+ This tests the new plugin management and enablement features.
+ """
+
+
+ if DEBUGGING:
+ # Add debugging steps(if any)...
+ pass
+
+ for plugin in topology.standalone.plugins.list():
+ print(plugin)
+
+
+ # Make sure that when we get the plugin for refint, it's the correct
+ # type
+ refintplugin = topology.standalone.plugins.get('libreferint-plugin')
+ assert(isinstance(refintplugin, ReferentialIntegrityPlugin))
+
+ # Set and configure a simple plugin (IE no extra config)
+
+ rolesplugin = topology.standalone.plugins.get('libroles-plugin')
+ assert(isinstance(rolesplugin, Plugin))
+
+ # Neither of these should fail
+ rolesplugin.disable()
+ rolesplugin.enable()
+
+ # Prove that we can set and configure a complex plugin
+
+ uniqplugin = topology.standalone.plugins.get('attribute uniqueness')
+ assert(isinstance(uniqplugin, AttributeUniquenessPlugin))
+
+ uniqplugin.disable()
+ uniqplugin.add_unique_attribute('sn')
+ uniqplugin.enable_all_subtrees()
+ uniqplugin.enable()
+
+ # Test compat with the old api
+
+ topology.standalone.plugins.enable('attribute uniqueness')
+
+ # Show the status
+
+ assert(uniqplugin.status())
+
+ log.info('Test PASSED')
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
+
+
diff --git a/src/lib389/lib389/tests/plugins/__init__.py
b/src/lib389/lib389/tests/plugins/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib389/lib389/tests/plugins/memberof_test.py
b/src/lib389/lib389/tests/plugins/memberof_test.py
new file mode 100644
index 0000000..9bafc60
--- /dev/null
+++ b/src/lib389/lib389/tests/plugins/memberof_test.py
@@ -0,0 +1,354 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016-2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+
+import pytest
+from time import sleep
+
+from lib389.topologies import topology_st
+from lib389.plugins import MemberOfPlugin
+from lib389.properties import BACKEND_NAME, BACKEND_SUFFIX
+from lib389._constants import DEFAULT_SUFFIX
+from lib389.tests.plugins.utils import (
+ create_test_user, create_test_group, create_test_ou, delete_objects)
+
+
+(a)pytest.fixture(scope="module")
+def plugin(request):
+ topology = topology_st(request)
+ plugin = MemberOfPlugin(topology.standalone)
+ return plugin
+
+
+def test_memberof_enable_disable(plugin):
+ """
+ Test that the plugin doesn't do anything while disabled, and functions
+ properly when enabled.
+
+ NOTICE: This test case leaves the plugin enabled for the following tests.
+ """
+ # assert plugin is disabled (by default)
+ assert plugin.status() == False
+
+ user = create_test_user(plugin._instance)
+ group = create_test_group(plugin._instance)
+
+ # add user to the group (which normally triggers the plugin)
+ group.add_member(user.dn)
+
+ memberofattr = plugin.get_attr()
+ # assert no memberof attribute was created by the plugin
+ assert not memberofattr in user.get_all_attrs()
+
+ # enable the plugin and restart the server for the action to take effect
+ plugin.enable()
+ plugin._instance.restart()
+ assert plugin.status() == True
+
+ # trigger the plugin again
+ group.remove_member(user.dn)
+ group.add_member(user.dn)
+
+ # assert that the memberof attribute was now properly set
+ assert memberofattr in user.get_all_attrs()
+
+ # clean up for subsequent test cases
+ delete_objects([user, group])
+
+def test_memberofattr(plugin):
+ """
+ Test that the plugin automatically creates an attribute on user entries
+ declaring membership after adding them to a group. The attribute depends
+ on the value of memberOfAttr.
+ """
+ memberofattr = "memberOf"
+ plugin.set_attr(memberofattr)
+
+ user = create_test_user(plugin._instance)
+ group = create_test_group(plugin._instance)
+
+ # memberof attribute should not yet appear
+ assert not memberofattr in user.get_all_attrs()
+
+ # trigger the plugin
+ group.add_member(user.dn)
+
+ # assert that the memberof attribute was automatically set by the plugin
+ assert group.dn in user.get_attr_vals(memberofattr)
+
+ # clean up for subsequent test cases
+ delete_objects([user, group])
+
+def test_memberofgroupattr(plugin):
+ """
+ memberOfGroupAttr gives the attribute in the group entry to poll to identify
+ member DNs. Test that the memberOf is set on a user only for groups whose
+ membership attribute is included in memberOfGroupAttr.
+ """
+ memberofattr = plugin.get_attr()
+
+ # initially "member" should be the default and only value of
memberOfGroupAttr
+ assert plugin.get_attr_vals('memberofgroupattr') == ['member']
+
+ user = create_test_user(plugin._instance)
+ group_normal = create_test_group(plugin._instance)
+ group_unique = create_test_group(plugin._instance, unique_group=True)
+
+ # add user to both groups
+ group_normal.add_member(user.dn)
+ group_unique.add_member(user.dn)
+
+ # assert that the memberof attribute was set for normal group only
+ assert group_normal.dn in user.get_attr_vals(memberofattr)
+ assert not group_unique.dn in user.get_attr_vals(memberofattr)
+
+ # add another value to memberOfGroupAttr
+ plugin.add_groupattr('uniqueMember')
+
+ # remove user from groups and add them again in order to trigger the plugin
+ group_normal.remove_member(user.dn)
+ group_normal.add_member(user.dn)
+ group_unique.remove_member(user.dn)
+ group_unique.add_member(user.dn)
+
+ # assert that the memberof attribute was set for both groups this time
+ assert group_normal.dn in user.get_attr_vals(memberofattr)
+ assert group_unique.dn in user.get_attr_vals(memberofattr)
+
+ # clean up for subsequent test cases
+ delete_objects([user, group_normal, group_unique])
+
+def test_memberofallbackends(plugin):
+ """
+ By default the MemberOf plugin only looks for potential members for users
+ who are in the same database as the group. Test that when memberOfAllBackends
+ is enabled, memberOf will search across all databases instead.
+ """
+ ou_value = "People2"
+ ou_suffix = DEFAULT_SUFFIX
+
+ # create a new backend
+ plugin._instance.backends.create(
+ None, properties={
+ BACKEND_NAME: "People2Data",
+ BACKEND_SUFFIX: "ou=" + ou_value + "," + ou_suffix,
+ })
+
+ # create a new sub-suffix stored in the new backend
+ ou2 = create_test_ou(plugin._instance, ou=ou_value, suffix=ou_suffix)
+
+ # add a user in the default backend
+ user_b1 = create_test_user(plugin._instance)
+ # add a user in the new backend
+ user_b2 = create_test_user(plugin._instance, suffix=ou2.dn)
+ # create a group in the default backend
+ group = create_test_group(plugin._instance)
+
+ # configure memberof to search only for users who are in the same backend as the
group
+ plugin.disable_allbackends()
+
+ group.add_member(user_b1.dn)
+ group.add_member(user_b2.dn)
+
+ memberofattr = plugin.get_attr()
+
+ # assert that memberOfAttr was set for the user stored in the same backend as the
group
+ assert group.dn in user_b1.get_attr_vals(memberofattr)
+ # assert that memberOfAttr was NOT set for the user stored in a different backend
+ assert not memberofattr in user_b2.get_all_attrs()
+
+ # configure memberof to search across all backends
+ plugin.enable_allbackends()
+
+ # remove users from group and add them again in order to re-trigger the plugin
+ group.remove_member(user_b1.dn)
+ group.remove_member(user_b2.dn)
+ group.add_member(user_b1.dn)
+ group.add_member(user_b2.dn)
+
+ # assert that memberOfAttr was set for users stored in both backends
+ assert group.dn in user_b1.get_attr_vals(memberofattr)
+ assert group.dn in user_b2.get_attr_vals(memberofattr)
+
+ # clean up for subsequent test cases
+ delete_objects([user_b1, user_b2, group, ou2])
+
+def test_memberofskipnested(plugin):
+ """
+ Test that when memberOfSkipNested is off (default) they plugin can properly
+ handle nested groups (groups that are member of other groups). Respectively
+ make sure that when memberOfSkipNested is on, the plugin lists only groups
+ to which a user was added directly.
+ """
+ user = create_test_user(plugin._instance)
+ group1 = create_test_group(plugin._instance)
+ group2 = create_test_group(plugin._instance)
+
+ # don't skip nested groups (this is the default)
+ plugin.disable_skipnested()
+
+ # create a nested group by listing group1 as a member of group2
+ group2.add_member(group1.dn)
+ # add user to group1 only
+ group1.add_member(user.dn)
+
+ memberofattr = plugin.get_attr()
+
+ assert group2.dn in group1.get_attr_vals(memberofattr)
+ # assert that memberOfAttr of user includes both groups
+ # even though they were not directly added to group2
+ assert group1.dn in user.get_attr_vals(memberofattr)
+ assert group2.dn in user.get_attr_vals(memberofattr)
+
+ # skip nested groups
+ plugin.enable_skipnested()
+
+ # remove from groups and add again in order to re-trigger the plugin
+ group2.remove_member(group1.dn)
+ group1.remove_member(user.dn)
+ group2.add_member(group1.dn)
+ group1.add_member(user.dn)
+
+ assert group2.dn in group1.get_attr_vals(memberofattr)
+ # assert that user's memberOfAttr includes only the group to which they were
added
+ assert group1.dn in user.get_attr_vals(memberofattr)
+ assert not group2.dn in user.get_attr_vals(memberofattr)
+
+ # clean up for subsequent test cases
+ delete_objects([user, group1, group2])
+
+def test_memberofautoaddocc(plugin):
+ """
+ Test that the MemberOf plugin automatically adds the object class defined
+ by memberOfAutoAddOC to a user object, if it does not contain an object
+ class that allows the memberOf attribute.
+ """
+ user = create_test_user(plugin._instance)
+ group = create_test_group(plugin._instance)
+
+ # delete any object classes that allow the memberOf attribute
+ if "nsMemberOf" in user.get_attr_vals("objectClass"):
+ user.remove("objectClass", "nsMemberOf")
+ if "inetUser" in user.get_attr_vals("objectClass"):
+ user.remove("objectClass", "inetUser")
+ if "inetAdmin" in user.get_attr_vals("objectClass"):
+ user.remove("objectClass", "inetAdmin")
+
+ # set a valid object class to memberOfAutoAddOC
+ plugin.set_autoaddoc("nsMemberOf")
+ # assert that user has not got this object class at the moment
+ assert not "nsMemberOf" in user.get_attr_vals("objectClass")
+
+ # trigger the plugin
+ group.add_member(user.dn)
+
+ # assert that the object class defined by memberOfAutoAddOC now exists
+ assert "nsMemberOf" in user.get_attr_vals("objectClass")
+
+ # reset user entry
+ group.remove_member(user.dn)
+ user.remove("objectClass", "nsMemberOf")
+
+ # repeat for different object class
+ plugin.set_autoaddoc("inetUser")
+ assert not "inetUser" in user.get_attr_vals("objectClass")
+
+ # re-trigger the plugin
+ group.add_member(user.dn)
+
+ assert "inetUser" in user.get_attr_vals("objectClass")
+
+ group.remove_member(user.dn)
+ user.remove("objectClass", "inetUser")
+
+ # repeat for different object class
+ plugin.set_autoaddoc("inetAdmin")
+ assert not "inetAdmin" in user.get_attr_vals("objectClass")
+
+ # re-trigger the plugin
+ group.add_member(user.dn)
+
+ assert "inetAdmin" in user.get_attr_vals("objectClass")
+
+ # clean up for subsequent test cases
+ delete_objects([user, group])
+
+def test_scoping(plugin):
+ """
+ Tests that the MemberOf plugin works on suffixes listed in memberOfEntryScope,
+ but skips suffixes listed in memberOfEntryScopeExcludeSubtree.
+ """
+ # create a new ou and 2 users under different ous
+ ou2 = create_test_ou(plugin._instance)
+ user_p1 = create_test_user(plugin._instance)
+ user_p2 = create_test_user(plugin._instance, suffix=ou2.dn)
+ group = create_test_group(plugin._instance)
+
+ # define include and exclude suffixes for MemberOf
+ plugin.add_entryscope(DEFAULT_SUFFIX)
+ plugin.add_excludescope(ou2.dn)
+
+ group.add_member(user_p1.dn)
+ group.add_member(user_p2.dn)
+
+ memberofattr = plugin.get_attr()
+
+ # assert that memberOfAttr was set for entry of an included suffix
+ assert group.dn in user_p1.get_attr_vals(memberofattr)
+ # assert that memberOfAttr was NOT set for entry of an excluded suffix
+ assert not memberofattr in user_p2.get_all_attrs()
+
+ # clean up for subsequent test cases
+ delete_objects([user_p1, user_p2, group, ou2])
+
+ plugin.remove_all_excludescope()
+ plugin.remove_all_entryscope()
+
+def test_fixup_task(plugin):
+ """
+ Test that after creating the fix-up task entry, initial memberOf attributes
+ are created on the member's user entries in the directory automatically.
+ Also, test that the filter provided to the task is correctly applied.
+ """
+ # disable the plugin and restart the server for the action to take effect
+ plugin.disable()
+ plugin._instance.restart()
+
+ user1 = create_test_user(plugin._instance)
+ user2 = create_test_user(plugin._instance, cn="testuser2")
+ group = create_test_group(plugin._instance)
+
+ group.add_member(user1.dn)
+ group.add_member(user2.dn)
+
+ # enable the plugin and restart the server for the action to take effect
+ plugin.enable()
+ plugin._instance.restart()
+
+ memberofattr = plugin.get_attr()
+ # memberof attribute should not appear on user entries
+ assert not memberofattr in user1.get_all_attrs()
+ assert not memberofattr in user2.get_all_attrs()
+
+ # run the fix-up task and provide a filter for the entry
+ task = plugin.fixup(basedn=DEFAULT_SUFFIX, _filter="(cn=testuser2)")
+ # wait for the task to complete
+ task.wait()
+
+ assert task.is_complete()
+ assert task.get_exit_code() == 0
+
+ # memberof attribute should now appear on the user entry matching the filter
+ assert memberofattr in user2.get_all_attrs()
+ assert group.dn in user2.get_attr_vals(memberofattr)
+
+ # but should not appear on user entry that doesn't match the filter
+ assert not memberofattr in user1.get_all_attrs()
+
+ # clean up for subsequent test cases
+ delete_objects([user1, user2, group])
diff --git a/src/lib389/lib389/tests/plugins/referint_test.py
b/src/lib389/lib389/tests/plugins/referint_test.py
new file mode 100644
index 0000000..53500d9
--- /dev/null
+++ b/src/lib389/lib389/tests/plugins/referint_test.py
@@ -0,0 +1,83 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016-2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+
+import pytest
+
+from lib389.topologies import topology_st
+from lib389.plugins import ReferentialIntegrityPlugin
+from lib389.tests.plugins.utils import (
+ create_test_user, create_test_group, delete_objects)
+
+
+(a)pytest.fixture(scope="module")
+def plugin(request):
+ return ReferentialIntegrityPlugin(topology_st(request).standalone)
+
+
+def test_referint_enable_disable(plugin):
+ """
+ Test that the plugin doesn't do anything while disabled, and functions
+ properly when enabled.
+
+ NOTICE: This test case leaves the plugin enabled for the following tests.
+ """
+ # assert plugin is disabled (by default)
+ assert plugin.status() == False
+
+ user1 = create_test_user(plugin._instance)
+ group = create_test_group(plugin._instance)
+ group.add_member(user1.dn)
+
+ user1.delete()
+ # assert that user was not removed from group because the plugin is disabled
+ assert group.present(attr='member', value=user1.dn)
+
+ # enable the plugin and restart the server for the action to take effect
+ plugin.enable()
+ plugin._instance.restart()
+ assert plugin.status() == True
+
+ user2 = create_test_user(plugin._instance)
+ group.add_member(user2.dn)
+
+ user2.delete()
+ # assert that user was removed from the group as well
+ assert not group.present(attr='member', value=user2.dn)
+
+ # clean up for subsequent test cases
+ delete_objects([group])
+
+def test_membership_attr(plugin):
+ """
+ Test that the plugin performs integrity updates based on the attributes
+ defined by referint-membership-attr.
+ """
+ # remove a membership attribute
+ plugin.remove_membership_attr('uniquemember')
+
+ user1 = create_test_user(plugin._instance)
+ group = create_test_group(plugin._instance, unique_group=True)
+ group.add_member(user1.dn)
+
+ user1.delete()
+ # assert that the user was not removed from the group
+ assert group.present(attr='uniquemember', value=user1.dn)
+
+ # now put this membership attribute back and try again
+ plugin.add_membership_attr('uniquemember')
+
+ user2 = create_test_user(plugin._instance)
+ group.add_member(user2.dn)
+
+ user2.delete()
+ # assert that user was removed from the group as well
+ assert not group.present(attr='uniquemember', value=user2.dn)
+
+ # clean up for subsequent test cases
+ delete_objects([group])
diff --git a/src/lib389/lib389/tests/plugins/usn_test.py
b/src/lib389/lib389/tests/plugins/usn_test.py
new file mode 100644
index 0000000..186de98
--- /dev/null
+++ b/src/lib389/lib389/tests/plugins/usn_test.py
@@ -0,0 +1,240 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016-2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+
+import pytest
+
+from lib389.topologies import topology_st
+from lib389.plugins import USNPlugin
+from lib389.rootdse import RootDSE
+from lib389._mapped_object import DSLdapObjects
+from lib389.backend import Backend
+from lib389.tests.plugins.utils import create_test_user, create_test_ou, delete_objects
+from lib389._constants import DEFAULT_SUFFIX, DN_LDBM
+from lib389.properties import BACKEND_NAME, BACKEND_SUFFIX
+
+
+def reset_USN(plugin, objects):
+ """
+ Delete objects containing an entryusn attr, clean USN tombstone entries
+ and restart the instance. This makes USN to reset lastusn back to -1.
+ """
+ delete_objects(objects)
+ task = plugin.cleanup(suffix=DEFAULT_SUFFIX)
+ task.wait()
+ plugin._instance.restart()
+
+
+(a)pytest.fixture(scope="module")
+def plugin(request):
+ topology = topology_st(request)
+ plugin = USNPlugin(topology.standalone)
+ return plugin
+
+
+def test_usn_enable_disable(plugin):
+ """
+ Test that the plugin doesn't do anything while disabled, but stores
+ entryusn values properly when enabled.
+
+ NOTICE: This test case leaves the plugin enabled for the following tests.
+ """
+ # assert plugin is disabled (by default)
+ assert plugin.status() == False
+
+ root_dse = RootDSE(plugin._instance)
+ # create a function for returning lastusn every time is called
+ lastusn = lambda : root_dse.get_attr_val_int("lastusn;userroot")
+
+ user1 = create_test_user(plugin._instance)
+
+ # assert no entryusn,lastusn values were created while the plugin was disabled
+ assert not "entryusn" in user1.get_all_attrs()
+ assert not "lastusn;userroot" in root_dse.get_all_attrs()
+
+ # enable the plugin and restart the server for the action to take effect
+ plugin.enable()
+ plugin._instance.restart()
+ assert plugin.status() == True
+
+ assert lastusn() == -1
+ user2 = create_test_user(plugin._instance)
+ # assert that a new entry now contains the entryusn value
+ assert user2.get_attr_val_int("entryusn") == lastusn() == 0
+
+ # assert that USNs are properly assigned after any write operation
+ # write operations include add, modify, modrdn and delete operations
+ user3 = create_test_user(plugin._instance)
+ assert user3.get_attr_val_int("entryusn") == lastusn() == 1
+ user2.delete()
+ assert lastusn() == 2
+ user3.replace('sn', 'another surname')
+ assert user3.get_attr_val_int("entryusn") == lastusn() == 3
+
+ # reset USN for subsequent test cases
+ reset_USN(plugin, [user1, user3])
+
+def test_usn_local_mode(plugin):
+ """
+ Test that when USN is operating in local mode, each backend has an instance
+ of the USN Plug-in with a USN counter specific to that backend database.
+ """
+ # assert local mode
+ assert not plugin.is_global_mode_set()
+
+ ou_value = "People2"
+ ou_suffix = DEFAULT_SUFFIX
+
+ # create a new backend
+ plugin._instance.backends.create(
+ None, properties={
+ BACKEND_NAME: "People2Data",
+ BACKEND_SUFFIX: "ou=" + ou_value + "," + ou_suffix,
+ })
+
+ # create a new sub-suffix stored in the new backend
+ ou2 = create_test_ou(plugin._instance, ou=ou_value, suffix=ou_suffix)
+
+ root_dse = RootDSE(plugin._instance)
+ lastusn_b1 = lambda : root_dse.get_attr_val_int("lastusn;userroot")
+ lastusn_b2 = lambda : root_dse.get_attr_val_int("lastusn;people2data")
+
+ assert lastusn_b1() == lastusn_b2() == -1
+
+ # add a user in the default backend; trigger the plugin
+ user_b1 = create_test_user(plugin._instance)
+ user_b1.replace('sn', 'surname2')
+ user_b1.replace('sn', 'surname3')
+
+ # add a user in the new backend
+ user_b2 = create_test_user(plugin._instance, suffix=ou2.dn)
+
+ # assert the USN counter is different for each backend
+ assert user_b1.get_attr_val_int("entryusn") == lastusn_b1() == 2
+ assert user_b2.get_attr_val_int("entryusn") == lastusn_b2() == 0
+
+ # reset USN for subsequent test cases
+ b = Backend(plugin._instance, dn="cn=people2data," + DN_LDBM)
+ reset_USN(plugin, [user_b1, user_b2, ou2, b])
+
+def test_tombstone_cleanup(plugin):
+ """
+ Assert that the USN plugin removes tombstone entries when the cleanup task
+ is run. Test removal for a specific backend, a specific suffix, and up to
+ a given USN number.
+ """
+ # create a new backend and a new sub-suffix stored in the new backend
+ ou_value = "People3"
+ ou_suffix = DEFAULT_SUFFIX
+ plugin._instance.backends.create(
+ None, properties={
+ BACKEND_NAME: "People3Data",
+ BACKEND_SUFFIX: "ou=" + ou_value + "," + ou_suffix,
+ })
+
+ ou2 = create_test_ou(plugin._instance, ou=ou_value, suffix=ou_suffix)
+
+ tombstones_b1 = DSLdapObjects(plugin._instance)
+ tombstones_b1._basedn = "ou=People," + DEFAULT_SUFFIX
+ tombstones_b1._objectclasses = ['nsTombstone']
+
+ tombstones_b2 = DSLdapObjects(plugin._instance)
+ tombstones_b2._basedn = ou2.dn
+ tombstones_b2._objectclasses = ['nsTombstone']
+
+ root_dse = RootDSE(plugin._instance)
+ lastusn_b1 = lambda : root_dse.get_attr_val_int("lastusn;userroot")
+ assert lastusn_b1() == -1
+
+ user1_b1 = create_test_user(plugin._instance)
+ user2_b1 = create_test_user(plugin._instance)
+ user3_b1 = create_test_user(plugin._instance)
+
+ user1_b2 = create_test_user(plugin._instance, suffix=ou2.dn)
+ user2_b2 = create_test_user(plugin._instance, suffix=ou2.dn)
+
+ # assert no tombstones exist at this point
+ assert not tombstones_b1.list()
+ assert not tombstones_b2.list()
+
+ # create 3 tombstone entries on default backend
+ user1_b1.delete()
+ user2_b1.delete()
+ user3_b1.delete()
+
+ # assert there are 3 tombstone entries indeed on default backend
+ assert len(tombstones_b1.list()) == 3
+ assert not tombstones_b2.list()
+
+ assert lastusn_b1() == 5
+
+ # remove all tombstone entries from default backend, with a USN value up to 4
+ task = plugin.cleanup(suffix=DEFAULT_SUFFIX, max_usn=lastusn_b1()-1)
+ task.wait()
+
+ # assert all tombstone entries were deleted but the last one on default backend
+ assert len(tombstones_b1.list()) == 1
+ assert not tombstones_b2.list()
+
+ # create 2 tombstone entries on new backend
+ user1_b2.delete()
+ user2_b2.delete()
+
+ # assert there are 2 tombstone entries indeed on new backend
+ assert len(tombstones_b2.list()) == 2
+ assert len(tombstones_b1.list()) == 1
+
+ # remove all tombstone entries from ou2 suffix
+ task = plugin.cleanup(suffix=ou2.dn)
+ task.wait()
+
+ # assert there are no tombstone entries stored on ou2 suffix
+ assert not tombstones_b2.list()
+ assert len(tombstones_b1.list()) == 1
+
+ # reset USN for subsequent test cases
+ b = Backend(plugin._instance, dn="cn=people3data," + DN_LDBM)
+ reset_USN(plugin, [ou2, b])
+
+def test_usn_global_mode(plugin):
+ """
+ Test that when USN is operating in global mode, there is a global instance
+ of the USN Plug-in with a global USN counter that applies to changes made
+ to the entire directory (all backends).
+ """
+ plugin.enable_global_mode()
+ plugin._instance.restart()
+ assert plugin.is_global_mode_set()
+
+ # create a new backend and a new sub-suffix stored in the new backend
+ ou_value = "People4"
+ ou_suffix = DEFAULT_SUFFIX
+ plugin._instance.backends.create(
+ None, properties={
+ BACKEND_NAME: "People4Data",
+ BACKEND_SUFFIX: "ou=" + ou_value + "," + ou_suffix,
+ })
+
+ root_dse = RootDSE(plugin._instance)
+ assert "lastusn" in root_dse.get_all_attrs()
+
+ ou2 = create_test_ou(plugin._instance, ou=ou_value, suffix=ou_suffix)
+ assert ou2.get_attr_val_int("entryusn") == 0
+
+ # add a user in the default backend
+ user_b1 = create_test_user(plugin._instance)
+ # add a user in the new backend
+ user_b2 = create_test_user(plugin._instance, suffix=ou2.dn)
+
+ # assert that a global USN counter is used for all backends
+ assert user_b1.get_attr_val_int("entryusn") == 1
+ assert user_b2.get_attr_val_int("entryusn") == 2
+
+ # reset USN for subsequent test cases
+ b = Backend(plugin._instance, dn="cn=people4data," + DN_LDBM)
+ reset_USN(plugin, [user_b1, user_b2, ou2, b])
diff --git a/src/lib389/lib389/tests/plugins/utils.py
b/src/lib389/lib389/tests/plugins/utils.py
new file mode 100644
index 0000000..f5dfff4
--- /dev/null
+++ b/src/lib389/lib389/tests/plugins/utils.py
@@ -0,0 +1,124 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016-2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+
+from lib389.idm.user import UserAccount
+from lib389.idm.group import Group, UniqueGroup
+from lib389.idm.organisationalunit import OrganisationalUnit
+from lib389._constants import DEFAULT_SUFFIX
+
+
+# These globals are only used by the functions of this module.
+# Each time we create a new test user/group/ou we increment the corresponding
+# variable by 1, in order to ensure DN uniqueness.
+test_user_id = 0
+test_group_id = 0
+test_ou_id = 0
+
+
+def create_test_user(instance, cn=None, suffix=None):
+ """
+ Creates a new user for testing.
+
+ It tries to create a user that doesn't already exist by using a different
+ ID each time. However, if it is provided with an existing cn/suffix it
+ will fail to create a new user and it will raise an LDAP error.
+
+ Returns a UserAccount object.
+ """
+ global test_user_id
+
+ if cn is None:
+ cn = "testuser_" + str(test_user_id)
+ test_user_id += 1
+
+ if suffix is None:
+ suffix = "ou=People," + DEFAULT_SUFFIX
+ dn = "cn=" + cn + "," + suffix
+
+ properties = {
+ 'uid': cn,
+ 'cn': cn,
+ 'sn': 'user',
+ 'uidNumber': str(1000+test_user_id),
+ 'gidNumber': '2000',
+ 'homeDirectory': '/home/' + cn
+ }
+
+ user = UserAccount(instance, dn)
+ user.create(properties=properties)
+
+ return user
+
+def create_test_group(instance, cn=None, suffix=None, unique_group=False):
+ """
+ Creates a new group for testing.
+
+ It tries to create a group that doesn't already exist by using a different
+ ID each time. However, if it is provided with an existing cn/suffix it
+ will fail to create a new group and it will raise an LDAP error.
+
+ Returns a Group object.
+ """
+ global test_group_id
+
+ if cn is None:
+ cn = "testgroup_" + str(test_group_id)
+ test_group_id += 1
+
+ if suffix is None:
+ suffix = "ou=Groups," + DEFAULT_SUFFIX
+ dn = "cn=" + cn + "," + suffix
+
+ properties = {
+ 'cn': cn,
+ 'ou': 'groups',
+ }
+
+ if unique_group:
+ group = UniqueGroup(instance, dn)
+ else:
+ group = Group(instance, dn)
+
+ group.create(properties=properties)
+
+ return group
+
+def create_test_ou(instance, ou=None, suffix=None):
+ """
+ Creates a new Organizational Unit for testing.
+
+ It tries to create a ou that doesn't already exist by using a different
+ ID each time. However, if it is provided with an existing ou/suffix it
+ will fail to create a new ou and it will raise an LDAP error.
+
+ Returns an OrganisationalUnit object.
+ """
+ global test_ou_id
+
+ if ou is None:
+ ou = "TestOU_" + str(test_ou_id)
+ test_ou_id += 1
+
+ if suffix is None:
+ suffix = DEFAULT_SUFFIX
+ dn = ou + "," + suffix
+ dn = "ou=" + ou + "," + suffix
+
+ properties = {
+ 'ou': ou,
+ }
+
+ ou = OrganisationalUnit(instance, dn)
+ ou.create(properties=properties)
+
+ return ou
+
+def delete_objects(objects):
+ for obj in objects:
+ obj.delete()
diff --git a/src/lib389/lib389/tests/referral_test.py
b/src/lib389/lib389/tests/referral_test.py
new file mode 100644
index 0000000..2e54ab0
--- /dev/null
+++ b/src/lib389/lib389/tests/referral_test.py
@@ -0,0 +1,36 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+
+import ldap
+import logging
+
+from lib389.referral import Referrals, Referral
+
+from lib389.topologies import topology_st
+
+from lib389._constants import DEFAULT_SUFFIX
+
+log = logging.getLogger(__name__)
+
+def test_referral(topology_st):
+ standalone = topology_st.standalone
+
+ rs = Referrals(standalone, DEFAULT_SUFFIX)
+
+ r = rs.create(properties={
+ 'cn': 'testref',
+ 'ref': 'ldap://localhost:38901/ou=People,dc=example,dc=com'
+ })
+
+ r_all = rs.list()
+ assert(len(r_all) == 1)
+ r2 = r_all[0]
+ assert(r2.present('ref',
'ldap://localhost:38901/ou=People,dc=example,dc=com'))
+
+
diff --git a/src/lib389/lib389/tests/replicaLegacy_test.py
b/src/lib389/lib389/tests/replicaLegacy_test.py
new file mode 100644
index 0000000..72bd338
--- /dev/null
+++ b/src/lib389/lib389/tests/replicaLegacy_test.py
@@ -0,0 +1,457 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import ldap
+import os
+import pytest
+import logging
+
+from lib389 import InvalidArgumentError
+from lib389._constants import *
+from lib389.properties import *
+from lib389 import DirSrv, Entry
+
+logging.getLogger(__name__).setLevel(logging.DEBUG)
+log = logging.getLogger(__name__)
+
+# Used for One master / One consumer topology
+HOST_MASTER = LOCALHOST
+PORT_MASTER = 40389
+SERVERID_MASTER = 'master'
+REPLICAID_MASTER = 1
+
+HOST_CONSUMER = LOCALHOST
+PORT_CONSUMER = 50389
+SERVERID_CONSUMER = 'consumer'
+
+TEST_REPL_DN = "uid=test,%s" % DEFAULT_SUFFIX
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'dirsrv'
+INSTANCE_BACKUP = os.environ.get('BACKUPDIR', DEFAULT_BACKUPDIR)
+NEW_SUFFIX_1 = 'ou=test_master'
+NEW_BACKEND_1 = 'test_masterdb'
+NEW_RM_1 = "cn=replrepl,%s" % NEW_SUFFIX_1
+
+NEW_SUFFIX_2 = 'ou=test_consumer'
+NEW_BACKEND_2 = 'test_consumerdb'
+
+NEW_SUFFIX_3 = 'ou=test_enablereplication_1'
+NEW_BACKEND_3 = 'test_enablereplicationdb_1'
+
+NEW_SUFFIX_4 = 'ou=test_enablereplication_2'
+NEW_BACKEND_4 = 'test_enablereplicationdb_2'
+
+NEW_SUFFIX_5 = 'ou=test_enablereplication_3'
+NEW_BACKEND_5 = 'test_enablereplicationdb_3'
+
+
+class TopologyReplication(object):
+ def __init__(self, master, consumer):
+ master.open()
+ consumer.open()
+ self.master = master
+ self.consumer = consumer
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ # Create the master instance
+ master = DirSrv(verbose=False)
+ master.log.debug("Master allocated")
+ args = {SER_HOST: HOST_MASTER,
+ SER_PORT: PORT_MASTER,
+ SER_SERVERID_PROP: SERVERID_MASTER}
+ master.allocate(args)
+ if master.exists():
+ master.delete()
+ master.create()
+ master.open()
+
+ # Create the consumer instance
+ consumer = DirSrv(verbose=False)
+ consumer.log.debug("Consumer allocated")
+ args = {SER_HOST: HOST_CONSUMER,
+ SER_PORT: PORT_CONSUMER,
+ SER_SERVERID_PROP: SERVERID_CONSUMER}
+ consumer.allocate(args)
+ if consumer.exists():
+ consumer.delete()
+ consumer.create()
+ consumer.open()
+
+ # Delete each instance in the end
+ def fin():
+ master.delete()
+ consumer.delete()
+ request.addfinalizer(fin)
+
+ return TopologyReplication(master, consumer)
+
+
+def test_create(topology):
+ """This test creates
+ - suffix/backend (NEW_SUFFIX_[12], NEW_BACKEND_[12]) : Master
+ - suffix/backend (NEW_SUFFIX_[12], NEW_BACKEND_[12]) : Consumer
+ - replica NEW_SUFFIX_1 as MASTER : Master
+ - replica NEW_SUFFIX_2 as CONSUMER : Master
+ """
+
+ log.info("\n\n##########\n### CREATE\n############")
+ #
+ # MASTER (suffix/backend)
+ #
+ backendEntry = topology.master.backend.create(
+ suffix=NEW_SUFFIX_1, properties={BACKEND_NAME: NEW_BACKEND_1})
+ backendEntry = topology.master.backend.create(
+ suffix=NEW_SUFFIX_2, properties={BACKEND_NAME: NEW_BACKEND_2})
+
+ ents = topology.master.mappingtree.list()
+ master_nb_mappingtree = len(ents)
+
+ # create a first additional mapping tree
+ topology.master.mappingtree.create(NEW_SUFFIX_1, bename=NEW_BACKEND_1)
+ ents = topology.master.mappingtree.list()
+ assert len(ents) == (master_nb_mappingtree + 1)
+ topology.master.add_s(Entry((NEW_SUFFIX_1,
+ {'objectclass': "top
organizationalunit".split(),
+ 'ou': NEW_SUFFIX_1.split('=', 1)[1]})))
+
+ # create a second additional mapping tree
+ topology.master.mappingtree.create(NEW_SUFFIX_2, bename=NEW_BACKEND_2)
+ ents = topology.master.mappingtree.list()
+ assert len(ents) == (master_nb_mappingtree + 2)
+ topology.master.add_s(Entry((NEW_SUFFIX_2,
+ {'objectclass': "top
organizationalunit".split(),
+ 'ou': NEW_SUFFIX_2.split('=', 1)[1]})))
+ log.info('Master it exists now %d suffix(es)' % len(ents))
+
+ #
+ # CONSUMER (suffix/backend)
+ #
+ backendEntry = topology.consumer.backend.create(
+ suffix=NEW_SUFFIX_1, properties={BACKEND_NAME: NEW_BACKEND_1})
+ backendEntry = topology.consumer.backend.create(
+ suffix=NEW_SUFFIX_2, properties={BACKEND_NAME: NEW_BACKEND_2})
+
+ ents = topology.consumer.mappingtree.list()
+ consumer_nb_mappingtree = len(ents)
+
+ # create a first additional mapping tree
+ topology.consumer.mappingtree.create(NEW_SUFFIX_1, bename=NEW_BACKEND_1)
+ ents = topology.consumer.mappingtree.list()
+ assert len(ents) == (consumer_nb_mappingtree + 1)
+ topology.consumer.add_s(Entry((NEW_SUFFIX_1,
+ {'objectclass': "top
organizationalunit".split(),
+ 'ou': NEW_SUFFIX_1.split('=', 1)[1]})))
+
+ # create a second additional mapping tree
+ topology.consumer.mappingtree.create(NEW_SUFFIX_2, bename=NEW_BACKEND_2)
+ ents = topology.consumer.mappingtree.list()
+ assert len(ents) == (consumer_nb_mappingtree + 2)
+ topology.consumer.add_s(Entry((NEW_SUFFIX_2,
+ {'objectclass': "top
organizationalunit".split(),
+ 'ou': NEW_SUFFIX_2.split('=', 1)[1]})))
+ log.info('Consumer it exists now %d suffix(es)' % len(ents))
+
+ #
+ # Now create REPLICAS on master
+ #
+ # check it exists this entry to stores the changelogs
+ topology.master.changelog.create()
+
+ # create a master
+ topology.master.replica.create(suffix=NEW_SUFFIX_1,
+ role=ReplicaRole.MASTER,
+ rid=1)
+ ents = topology.master.replica.list()
+ assert len(ents) == 1
+ log.info('Master replica %s' % ents[0].dn)
+
+ # create a consumer
+ topology.master.replica.create(suffix=NEW_SUFFIX_2,
+ role=ReplicaRole.CONSUMER)
+ ents = topology.master.replica.list()
+ assert len(ents) == 2
+ ents = topology.master.replica.list(suffix=NEW_SUFFIX_2)
+ log.info('Consumer replica %s' % ents[0].dn)
+
+ #
+ # Now create REPLICAS on consumer
+ #
+ # create a master
+ topology.consumer.replica.create(suffix=NEW_SUFFIX_1,
+ role=ReplicaRole.CONSUMER)
+ ents = topology.consumer.replica.list()
+ assert len(ents) == 1
+ log.info('Consumer replica %s' % ents[0].dn)
+
+ # create a consumer
+ topology.consumer.replica.create(suffix=NEW_SUFFIX_2,
+ role=ReplicaRole.CONSUMER)
+ ents = topology.consumer.replica.list()
+ assert len(ents) == 2
+ ents = topology.consumer.replica.list(suffix=NEW_SUFFIX_2)
+ log.info('Consumer replica %s' % ents[0].dn)
+
+
+def test_list(topology):
+ """This test checks:
+ - existing replicas can be retrieved
+ - access to unknown replica does not fail
+
+ PRE-CONDITION:
+ It exists on MASTER two replicas NEW_SUFFIX_1 and NEW_SUFFIX_2
+ created by test_create()
+ """
+
+ log.info("\n\n############\n### LIST\n############")
+ ents = topology.master.replica.list()
+ assert len(ents) == 2
+
+ # Check we can retrieve a replica with its suffix
+ ents = topology.master.replica.list(suffix=NEW_SUFFIX_1)
+ assert len(ents) == 1
+ replica_dn_1 = ents[0].dn
+
+ # Check we can retrieve a replica with its suffix
+ ents = topology.master.replica.list(suffix=NEW_SUFFIX_2)
+ assert len(ents) == 1
+ replica_dn_2 = ents[0].dn
+
+ # Check we can retrieve a replica with its DN
+ ents = topology.master.replica.list(replica_dn=replica_dn_1)
+ assert len(ents) == 1
+ assert replica_dn_1 == ents[0].dn
+
+ # Check we can retrieve a replica if we provide DN and suffix
+ ents = topology.master.replica.list(suffix=NEW_SUFFIX_2,
+ replica_dn=replica_dn_2)
+ assert len(ents) == 1
+ assert replica_dn_2 == ents[0].dn
+
+ # Check DN is used before suffix name
+ ents = topology.master.replica.list(suffix=NEW_SUFFIX_2,
+ replica_dn=replica_dn_1)
+ assert len(ents) == 1
+ assert replica_dn_1 == ents[0].dn
+
+ # Check that invalid value does not break
+ ents = topology.master.replica.list(suffix="X")
+ for ent in ents:
+ log.critical("Unexpected replica: %s" % ent.dn)
+ assert len(ents) == 0
+
+
+def test_create_repl_manager(topology):
+ """The tests are
+ - create the default Replication manager/Password
+ - create a specific Replication manager/ default Password
+ - Check we can bind successfully
+ - create a specific Replication manager / specific Password
+ - Check we can bind successfully
+ """
+
+ log.info("\n\n###########\n### CREATE_REPL_MANAGER\n###########")
+ # First create the default replication manager
+ topology.consumer.replica.create_repl_manager()
+ ents = topology.consumer.search_s(defaultProperties[REPLICATION_BIND_DN],
+ ldap.SCOPE_BASE, "objectclass=*")
+ assert len(ents) == 1
+ assert ents[0].dn == defaultProperties[REPLICATION_BIND_DN]
+
+ # Second create a custom replication manager under NEW_SUFFIX_2
+ rm_dn = "cn=replrepl,%s" % NEW_SUFFIX_2
+ topology.consumer.replica.create_repl_manager(repl_manager_dn=rm_dn)
+ ents = topology.consumer.search_s(rm_dn, ldap.SCOPE_BASE, "objectclass=*")
+ assert len(ents) == 1
+ assert ents[0].dn == rm_dn
+
+ # Check we can bind
+ topology.consumer.simple_bind_s(rm_dn,
+ defaultProperties[REPLICATION_BIND_PW])
+
+ # Check we fail to bind
+ with pytest.raises(ldap.INVALID_CREDENTIALS) as excinfo:
+ topology.consumer.simple_bind_s(rm_dn, "dummy")
+ log.info("Exception: %s" % str(excinfo.value))
+
+ # now rebind
+ topology.consumer.simple_bind_s(topology.consumer.binddn,
+ topology.consumer.bindpw)
+
+ # Create a custom replication manager under NEW_SUFFIX_1
+ # with a specified password
+ rm_dn = NEW_RM_1
+ topology.consumer.replica.create_repl_manager(repl_manager_dn=rm_dn,
+ repl_manager_pw="Secret123")
+ ents = topology.consumer.search_s(rm_dn, ldap.SCOPE_BASE, "objectclass=*")
+ assert len(ents) == 1
+ assert ents[0].dn == rm_dn
+
+ # Check we can bind
+ topology.consumer.simple_bind_s(rm_dn, "Secret123")
+
+ # Check we fail to bind
+ with pytest.raises(ldap.INVALID_CREDENTIALS) as excinfo:
+ topology.consumer.simple_bind_s(rm_dn, "dummy")
+ log.info("Exception: %s" % str(excinfo.value))
+ topology.consumer.simple_bind_s(topology.consumer.binddn,
+ topology.consumer.bindpw)
+
+
+def test_enableReplication(topology):
+ """It checks
+ - Ability to enable replication on a supplier
+ - Ability to enable replication on a consumer
+ - Failure to enable replication with wrong replicaID on supplier
+ - Failure to enable replication with wrong replicaID on consumer
+ """
+
+ log.info("\n\n############\n### ENABLEREPLICATION\n##########")
+ #
+ # MASTER (suffix/backend)
+ #
+ backendEntry = topology.master.backend.create(suffix=NEW_SUFFIX_3,
+ properties={BACKEND_NAME:
+ NEW_BACKEND_3})
+
+ ents = topology.master.mappingtree.list()
+ master_nb_mappingtree = len(ents)
+
+ # create a first additional mapping tree
+ topology.master.mappingtree.create(NEW_SUFFIX_3, bename=NEW_BACKEND_3)
+ ents = topology.master.mappingtree.list()
+ assert len(ents) == (master_nb_mappingtree + 1)
+ topology.master.add_s(Entry((NEW_SUFFIX_3,
+ {'objectclass': "top
organizationalunit".split(),
+ 'ou': NEW_SUFFIX_3.split('=', 1)[1]})))
+
+ # a supplier should have replicaId in [1..CONSUMER_REPLICAID[
+ with pytest.raises(ValueError) as excinfo:
+ topology.master.replica.enableReplication(suffix=NEW_SUFFIX_3,
+ role=ReplicaRole.MASTER,
+ replicaId=CONSUMER_REPLICAID)
+ log.info("Exception (expected): %s" % str(excinfo.value))
+ topology.master.replica.enableReplication(suffix=NEW_SUFFIX_3,
+ role=ReplicaRole.MASTER,
+ replicaId=1)
+
+ #
+ # MASTER (suffix/backend)
+ #
+ backendEntry = topology.master.backend.create(suffix=NEW_SUFFIX_4,
+ properties={BACKEND_NAME:
+ NEW_BACKEND_4})
+
+ ents = topology.master.mappingtree.list()
+ master_nb_mappingtree = len(ents)
+
+ # create a first additional mapping tree
+ topology.master.mappingtree.create(NEW_SUFFIX_4, bename=NEW_BACKEND_4)
+ ents = topology.master.mappingtree.list()
+ assert len(ents) == (master_nb_mappingtree + 1)
+ topology.master.add_s(Entry((NEW_SUFFIX_4,
+ {'objectclass': "top
organizationalunit".split(),
+ 'ou': NEW_SUFFIX_4.split('=', 1)[1]})))
+
+ # A consumer should have CONSUMER_REPLICAID not '1'
+ with pytest.raises(ValueError) as excinfo:
+ topology.master.replica.enableReplication(suffix=NEW_SUFFIX_4,
+ role=ReplicaRole.CONSUMER,
+ replicaId=1)
+ log.info("Exception (expected): %s" % str(excinfo.value))
+ topology.master.replica.enableReplication(suffix=NEW_SUFFIX_4,
+ role=ReplicaRole.CONSUMER)
+
+
+def test_disableReplication(topology):
+ """It checks
+ - Ability to disable replication on a supplier
+ - Ability to disable replication on a consumer
+ - Failure to disable replication with wrong suffix on supplier
+ - Failure to disable replication with wrong suffix on consumer
+ """
+
+ log.info("\n\n############\n### DISABLEREPLICATION\n##########")
+ topology.master.replica.disableReplication(suffix=NEW_SUFFIX_3)
+ with pytest.raises(ldap.LDAPError) as excinfo:
+ topology.master.replica.disableReplication(suffix=NEW_SUFFIX_3)
+ log.info("Exception (expected): %s" % str(excinfo.value))
+
+ topology.master.replica.disableReplication(suffix=NEW_SUFFIX_4)
+ with pytest.raises(ldap.LDAPError) as excinfo:
+ topology.master.replica.disableReplication(suffix=NEW_SUFFIX_4)
+ log.info("Exception (expected): %s" % str(excinfo.value))
+
+
+def test_setProperties(topology):
+ """Set some properties
+ Verified that valid properties are set
+ Verified that invalid properties raise an Exception
+
+ PRE-REQUISITE: it exists a replica for NEW_SUFFIX_1
+ """
+
+ log.info("\n\n##########\n### SETPROPERTIES\n############")
+ # set valid values to SUFFIX_1
+ properties = {REPLICA_BINDDN: NEW_RM_1,
+ REPLICA_PURGE_INTERVAL: str(3600),
+ REPLICA_PURGE_DELAY: str(5 * 24 * 3600),
+ REPLICA_REFERRAL: "ldap://%s:1234/" % LOCALHOST}
+ topology.master.replica.setProperties(suffix=NEW_SUFFIX_1,
+ properties=properties)
+
+ # Check the values have been written
+ replicas = topology.master.replica.list(suffix=NEW_SUFFIX_1)
+ assert len(replicas) == 1
+ for prop in properties:
+ attr = REPLICA_PROPNAME_TO_ATTRNAME[prop]
+ val = replicas[0].getValue(attr)
+ log.info("Replica[%s] -> %s: %s" % (prop, attr, val))
+ assert val == properties[prop]
+
+ # Check invalid properties raise exception
+ with pytest.raises(ValueError) as excinfo:
+ properties = {"dummy": 'dummy'}
+ topology.master.replica.setProperties(suffix=NEW_SUFFIX_1,
+ properties=properties)
+ log.info("Exception (expected): %s" % str(excinfo.value))
+
+ # check call without suffix/dn/entry raise InvalidArgumentError
+ with pytest.raises(InvalidArgumentError) as excinfo:
+ properties = {REPLICA_BINDDN: NEW_RM_1}
+ topology.master.replica.setProperties(properties=properties)
+ log.info("Exception (expected): %s" % str(excinfo.value))
+
+ # check that if we do not provide a valid entry it raises ValueError
+ with pytest.raises(ValueError) as excinfo:
+ properties = {REPLICA_BINDDN: NEW_RM_1}
+ topology.master.replica.setProperties(replica_entry="dummy",
+ properties=properties)
+ log.info("Exception (expected): %s" % str(excinfo.value))
+
+ # check that with an invalid suffix or replica_dn it raise ValueError
+ with pytest.raises(ValueError) as excinfo:
+ properties = {REPLICA_BINDDN: NEW_RM_1}
+ topology.master.replica.setProperties(suffix="dummy",
+ properties=properties)
+ log.info("Exception (expected): %s" % str(excinfo.value))
+
+
+def test_getProperties(topology):
+ """Currently not implemented"""
+
+ log.info("\n\n############\n### GETPROPERTIES\n###########")
+ with pytest.raises(NotImplementedError) as excinfo:
+ topology.master.replica.getProperties(suffix=NEW_SUFFIX_1)
+ log.info("Exception (expected): %s" % str(excinfo.value))
+
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/replica_test.py
b/src/lib389/lib389/tests/replica_test.py
new file mode 100644
index 0000000..7250d67
--- /dev/null
+++ b/src/lib389/lib389/tests/replica_test.py
@@ -0,0 +1,375 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import os
+import ldap
+import pytest
+import logging
+
+from lib389 import NoSuchEntryError
+from lib389.replica import Replicas
+from lib389.backend import Backends
+from lib389.idm.domain import Domain
+from lib389._constants import (ReplicaRole, BACKEND_SUFFIX, BACKEND_NAME,
REPLICA_RUV_FILTER, CONSUMER_REPLICAID,
+ REPLICA_FLAGS_WRITE, REPLICA_RDWR_TYPE)
+from lib389.properties import (REPL_FLAGS, REPL_TYPE)
+from lib389.topologies import topology_i3 as topo
+
+logging.getLogger(__name__).setLevel(logging.DEBUG)
+log = logging.getLogger(__name__)
+
+DEBUGGING = os.getenv('DEBUGGING', default=False)
+NEW_SUFFIX = 'dc=test,dc=com'
+NEW_BACKEND = 'test_backend'
+REPLICA_MASTER_ID = 1
+
+
+(a)pytest.fixture(scope="module")
+def new_suffixes(topo):
+ """Create new suffix, backend and mapping tree"""
+
+ for num in range(1, 4):
+ backends = Backends(topo.ins["standalone{}".format(num)])
+ backends.create(properties={BACKEND_SUFFIX: NEW_SUFFIX,
+ BACKEND_NAME: NEW_BACKEND})
+ domain = Domain(topo.ins["standalone{}".format(num)], NEW_SUFFIX)
+ domain.create(properties={'dc': 'test', 'description':
NEW_SUFFIX})
+
+
+(a)pytest.fixture(scope="function")
+def simple_replica(topo, new_suffixes, request):
+ """Enable simple multi-master replication"""
+
+ master1 = topo.ins["standalone1"]
+ master2 = topo.ins["standalone2"]
+
+ log.info("Enable two master replicas")
+ replicas_m1 = Replicas(master1)
+ replica_m1 = replicas_m1.enable(suffix=NEW_SUFFIX,
+ role=ReplicaRole.MASTER,
+ replicaID=REPLICA_MASTER_ID)
+ replicas_m2 = Replicas(master2)
+ replica_m2 = replicas_m2.enable(suffix=NEW_SUFFIX,
+ role=ReplicaRole.MASTER,
+ replicaID=REPLICA_MASTER_ID+1)
+
+ log.info("Create agreements between the instances")
+ master1.agreement.create(suffix=NEW_SUFFIX,
+ host=master2.host,
+ port=master2.port)
+ master2.agreement.create(suffix=NEW_SUFFIX,
+ host=master1.host,
+ port=master1.port)
+
+ log.info("Test replication")
+ replicas_m1.test(NEW_SUFFIX, master2)
+
+ def fin():
+ replicas_m1.disable(NEW_SUFFIX)
+ replicas_m2.disable(NEW_SUFFIX)
+ request.addfinalizer(fin)
+
+ return [replica_m1, replica_m2]
+
+
+(a)pytest.fixture(scope="function")
+def clean_up(topo, new_suffixes, request):
+ """Check that all replicas were disabled and disable if
not"""
+
+ def fin():
+ for num in range(1, 4):
+ try:
+ replicas = Replicas(topo.ins["standalone{}".format(num)])
+ replicas.disable(NEW_SUFFIX)
+ log.info("standalone{} is disabled now".format(num))
+ except:
+ pass
+ request.addfinalizer(fin)
+
+
+def test_delete_agreements(topo, simple_replica):
+ """Check deleteAgreements method
+
+ :feature: Replication
+ :steps: 1. Enable replication with agreements
+ 2. Delete the agreements
+ 3. Check that agreements were deleted
+ 4. Disable replication
+ :expectedresults: No errors happen, agreements successfully deleted
+ """
+
+ master1 = topo.ins["standalone1"]
+ master2 = topo.ins["standalone2"]
+
+ log.info("Check that agreements in place")
+ ents = master1.agreement.list(suffix=NEW_SUFFIX)
+ assert(len(ents) == 1)
+ ents = master2.agreement.list(suffix=NEW_SUFFIX)
+ assert(len(ents) == 1)
+
+ log.info("Delete the agreements")
+ simple_replica[0].deleteAgreements()
+ simple_replica[1].deleteAgreements()
+
+ log.info("Check that agreements were deleted")
+ ents = master1.agreement.list(suffix=NEW_SUFFIX)
+ assert(len(ents) == 0)
+ ents = master2.agreement.list(suffix=NEW_SUFFIX)
+ assert(len(ents) == 0)
+
+
+def test_get_ruv_entry(topo, simple_replica):
+ """Check get_ruv_entry method
+
+ :feature: Replication
+ :steps: 1. Enable replication with agreements
+ 2. Get ruv entry with get_ruv_entry() method
+ 3. Get ruv entry with ldap.search
+ 4. Disable replication
+ :expectedresults: Entries should be equal
+ """
+
+ ruv_entry = simple_replica[0].get_ruv_entry()
+ entry = topo.ins["standalone1"].search_s(NEW_SUFFIX, ldap.SCOPE_SUBTREE,
REPLICA_RUV_FILTER)[0]
+
+ assert ruv_entry == entry
+
+
+def test_get_role(topo, simple_replica):
+ """Check get_role method
+
+ :feature: Replication
+ :steps: 1. Enable replication with agreements
+ 2. Get role with get_role() method
+ 3. Get repl_flags, repl_type from the replica entry with ldap.search
+ 4. Compare the values
+ 5. Disable replication
+ :expectedresults: The role 'master' should have flags=1 and type=3
+ """
+
+ replica_role = simple_replica[0].get_role()
+ replica_flags = simple_replica[0].get_attr_val_int(REPL_FLAGS)
+ replica_type = simple_replica[0].get_attr_val_int(REPL_TYPE)
+
+ log.info("Check that we've got role 'master', while {}=1 and
{}=3".format(REPL_FLAGS, REPL_TYPE))
+ assert replica_role == ReplicaRole.MASTER and replica_flags == REPLICA_FLAGS_WRITE \
+ and replica_type == REPLICA_RDWR_TYPE, \
+ "Failure, get_role() gave {}, while {} has {} and {} has
{}".format(replica_role,
+ REPL_FLAGS,
replica_flags,
+ REPL_TYPE,
replica_type)
+
+
+def test_basic(topo, new_suffixes, clean_up):
+ """Check basic replica functionality
+
+ :feature: Replication
+ :steps: 1. Enable replication on master. hub and consumer
+ 2. Create agreements: master-hub, hub-consumer
+ 3. Test master-consumer replication
+ 4. Disable replication
+ 5. Check that replica, agreements and changelog were deleted
+ :expectedresults: No errors happen, replication is successfully enabled and disabled
+ """
+
+ master = topo.ins["standalone1"]
+ hub = topo.ins["standalone2"]
+ consumer = topo.ins["standalone3"]
+
+ log.info("Enable replicas (create replica and changelog entries)")
+ master_replicas = Replicas(master)
+ master_replicas.enable(suffix=NEW_SUFFIX,
+ role=ReplicaRole.MASTER,
+ replicaID=REPLICA_MASTER_ID)
+ ents = master_replicas.list()
+ assert len(ents) == 1
+ ents = master.changelog.list()
+ assert len(ents) == 1
+
+ hub_replicas = Replicas(hub)
+ hub_replicas.enable(suffix=NEW_SUFFIX,
+ role=ReplicaRole.HUB,
+ replicaID=CONSUMER_REPLICAID)
+ ents = hub_replicas.list()
+ assert len(ents) == 1
+ ents = hub.changelog.list()
+ assert len(ents) == 1
+
+ consumer_replicas = Replicas(consumer)
+ consumer_replicas.enable(suffix=NEW_SUFFIX,
+ role=ReplicaRole.CONSUMER)
+ ents = consumer_replicas.list()
+ assert len(ents) == 1
+
+ log.info("Create agreements between the instances")
+ master.agreement.create(suffix=NEW_SUFFIX,
+ host=hub.host,
+ port=hub.port)
+ ents = master.agreement.list(suffix=NEW_SUFFIX)
+ assert len(ents) == 1
+ hub.agreement.create(suffix=NEW_SUFFIX,
+ host=consumer.host,
+ port=consumer.port)
+ ents = hub.agreement.list(suffix=NEW_SUFFIX)
+ assert len(ents) == 1
+
+ log.info("Test replication")
+ master_replicas.test(NEW_SUFFIX, consumer)
+
+ log.info("Disable replication")
+ master_replicas.disable(suffix=NEW_SUFFIX)
+ hub_replicas.disable(suffix=NEW_SUFFIX)
+ consumer_replicas.disable(suffix=NEW_SUFFIX)
+
+ log.info("Check that replica, agreements and changelog were deleted")
+ for num in range(1, 4):
+ log.info("Checking standalone{} instance".format(num))
+ inst = topo.ins["standalone{}".format(num)]
+
+ log.info("Checking that replica entries don't exist")
+ replicas = Replicas(inst)
+ ents = replicas.list()
+ assert len(ents) == 0
+
+ log.info("Checking that changelog doesn't exist")
+ ents = inst.changelog.list()
+ assert len(ents) == 0
+
+ log.info("Checking that agreements can't be acquired because the replica
entry doesn't exist")
+ with pytest.raises(NoSuchEntryError) as e:
+ inst.agreement.list(suffix=NEW_SUFFIX)
+ assert "no replica set up" in e.msg
+
+
+(a)pytest.mark.parametrize('role_from,role_to',
+ ((ReplicaRole.CONSUMER, ReplicaRole.HUB),
+ (ReplicaRole.CONSUMER, ReplicaRole.MASTER),
+ (ReplicaRole.HUB, ReplicaRole.MASTER)))
+def test_promote(topo, new_suffixes, clean_up, role_from, role_to):
+ """Check that replica promote method works properly
+
+ :feature: Replication
+ :steps: 1. Enable replication on the instance
+ 2. Promote it to another role
+ (check consumer-hub, consumer-master, hub-master
+ 3. Check that role was successfully changed
+ 4. Disable replication
+ :expectedresults: No errors happen, replica successfully promoted
+ """
+
+ inst = topo.ins["standalone1"]
+
+ log.info("Enable replication on instance with a role -
{}".format(role_from))
+ replicas = Replicas(inst)
+ replica = replicas.enable(suffix=NEW_SUFFIX,
+ role=role_from)
+
+ log.info("Promote replica to {}".format(role_to))
+ replica.promote(newrole=role_to,
+ rid=REPLICA_MASTER_ID)
+
+ log.info("Check that replica was successfully promoted")
+ replica_role = replica.get_role()
+ assert replica_role == role_to
+
+
+(a)pytest.mark.parametrize('role_from,role_to',
+ ((ReplicaRole.MASTER, ReplicaRole.HUB),
+ (ReplicaRole.MASTER, ReplicaRole.CONSUMER),
+ (ReplicaRole.HUB, ReplicaRole.CONSUMER)))
+def test_demote(topo, new_suffixes, clean_up, role_from, role_to):
+ """Check that replica demote method works properly
+
+ :feature: Replication
+ :steps: 1. Enable replication on the instance
+ 2. Demote it to another role
+ (check master-hub, master-consumer, hub-consumer)
+ 3. Check that role was successfully changed
+ 4. Disable replication
+ :expectedresults: No errors happen, replica successfully demoted
+ """
+
+ inst = topo.ins["standalone1"]
+
+ log.info("Enable replication on instance with a role -
{}".format(role_from.name))
+ replicas = Replicas(inst)
+ replica = replicas.enable(suffix=NEW_SUFFIX,
+ role=role_from,
+ replicaID=REPLICA_MASTER_ID)
+
+ log.info("Promote replica to {}".format(role_to.name))
+ replica.demote(newrole=role_to)
+
+ log.info("Check that replica was successfully promoted")
+ replica_role = replica.get_role()
+ assert replica_role == role_to
+
+
+(a)pytest.mark.parametrize('role_from', (ReplicaRole.MASTER,
+ ReplicaRole.HUB,
+ ReplicaRole.CONSUMER))
+def test_promote_fail(topo, new_suffixes, clean_up, role_from):
+ """Check that replica promote method fails
+ when promoted to wrong direction
+
+ :feature: Replication
+ :steps: 1. Enable replication on the instance
+ 2. Try to promote it to wrong role
+ (for example, master-hub, hub-consumer)
+ 3. Disable replication
+ :expectedresults: Replica shouldn't be promoted
+ """
+
+ inst = topo.ins["standalone1"]
+
+ log.info("Enable replication on instance with a role -
{}".format(role_from.name))
+ replicas = Replicas(inst)
+ replica = replicas.enable(suffix=NEW_SUFFIX,
+ role=role_from,
+ replicaID=REPLICA_MASTER_ID)
+
+ for role_to in [x for x in range(1, 4) if x <= role_from.value]:
+ role_to = ReplicaRole(role_to)
+ log.info("Try to promote replica to {}".format(role_to.name))
+ with pytest.raises(ValueError):
+ replica.promote(newrole=role_to,
+ rid=REPLICA_MASTER_ID)
+
+
+(a)pytest.mark.parametrize('role_from', (ReplicaRole.MASTER,
+ ReplicaRole.HUB,
+ ReplicaRole.CONSUMER))
+def test_demote_fail(topo, new_suffixes, clean_up, role_from):
+ """Check that replica demote method fails
+ when demoted to wrong direction
+
+ :feature: Replication
+ :steps: 1. Enable replication on the instance
+ 2. Try to demote it to wrong role
+ (for example, consumer-master, hub-master)
+ 3. Disable replication
+ :expectedresults: Replica shouldn't be demoted
+ """
+
+ inst = topo.ins["standalone1"]
+
+ log.info("Enable replication on instance with a role -
{}".format(role_from.name))
+ replicas = Replicas(inst)
+ replica = replicas.enable(suffix=NEW_SUFFIX,
+ role=role_from,
+ replicaID=REPLICA_MASTER_ID)
+
+ for role_to in [x for x in range(1, 4) if x >= role_from.value]:
+ role_to = ReplicaRole(role_to)
+ log.info("Try to demote replica to {}".format(role_to.name))
+ with pytest.raises(ValueError):
+ replica.demote(newrole=role_to)
+
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/schema_test.py
b/src/lib389/lib389/tests/schema_test.py
new file mode 100644
index 0000000..28daefe
--- /dev/null
+++ b/src/lib389/lib389/tests/schema_test.py
@@ -0,0 +1,72 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import pytest
+
+from lib389 import DirSrv
+from lib389._constants import SER_HOST, SER_PORT, SER_SERVERID_PROP, LOCALHOST
+from lib389.schema import Schema
+
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+
+
+class TopologyInstance(object):
+ def __init__(self, instance):
+ instance.open()
+ self.instance = instance
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ instance = DirSrv(verbose=False)
+ instance.log.debug("Instance allocated")
+ args = {SER_HOST: LOCALHOST,
+ SER_PORT: INSTANCE_PORT,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ instance.allocate(args)
+ if instance.exists():
+ instance.delete()
+ instance.create()
+ instance.open()
+
+ def fin():
+ instance.delete()
+ request.addfinalizer(fin)
+
+ return TopologyInstance(instance)
+
+
+def test_schema(topology):
+ must_expect = ['uidObject', 'account', 'posixAccount',
'shadowAccount']
+ may_expect = ['cosDefinition', 'inetOrgPerson', 'inetUser',
+ 'mailRecipient']
+ attrtype, must, may = topology.instance.schema.query_attributetype('uid')
+ assert attrtype.names == ('uid', 'userid')
+ for oc in must:
+ assert oc.names[0] in must_expect
+ for oc in may:
+ assert oc.names[0] in may_expect
+ assert topology.instance.schema.query_objectclass('account').names == \
+ ('account', )
+
+def test_schema_reload(topology):
+ """Test that the appropriate task entry is created when reloading
schema."""
+ schema = Schema(topology.instance)
+ task = schema.reload()
+ assert task.exists()
+ task.wait()
+ assert task.get_exit_code() == 0
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ import os
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/suffix_test.py
b/src/lib389/lib389/tests/suffix_test.py
new file mode 100644
index 0000000..df75e6d
--- /dev/null
+++ b/src/lib389/lib389/tests/suffix_test.py
@@ -0,0 +1,105 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import os
+import pytest
+from lib389._constants import *
+from lib389.properties import *
+from lib389 import DirSrv
+
+
+TEST_REPL_DN = "uid=test,%s" % DEFAULT_SUFFIX
+INSTANCE_PORT = 54321
+INSTANCE_SERVERID = 'standalone'
+# INSTANCE_PREFIX = os.environ.get('PREFIX', None)
+INSTANCE_PREFIX = None
+INSTANCE_BACKUP = os.environ.get('BACKUPDIR', DEFAULT_BACKUPDIR)
+NEW_SUFFIX_1 = 'o=test_create'
+NEW_BACKEND_1 = 'test_createdb'
+NEW_CHILDSUFFIX_1 = 'o=child1,o=test_create'
+NEW_CHILDBACKEND_1 = 'test_createchilddb'
+NEW_SUFFIX_2 = 'o=test_bis_create'
+NEW_BACKEND_2 = 'test_bis_createdb'
+NEW_CHILDSUFFIX_2 = 'o=child2,o=test_bis_create'
+NEW_CHILDBACKEND_2 = 'test_bis_createchilddb'
+
+
+class TopologyStandalone(object):
+ def __init__(self, instance):
+ instance.open()
+ self.instance = instance
+
+
+(a)pytest.fixture(scope="module")
+def topology(request):
+ instance = DirSrv(verbose=False)
+ instance.log.debug("Instance allocated")
+ args = {SER_HOST: LOCALHOST,
+ SER_PORT: INSTANCE_PORT,
+ SER_DEPLOYED_DIR: INSTANCE_PREFIX,
+ SER_SERVERID_PROP: INSTANCE_SERVERID}
+ instance.allocate(args)
+ if instance.exists():
+ instance.delete()
+ instance.create()
+ instance.open()
+
+ def fin():
+ instance.delete()
+ request.addfinalizer(fin)
+
+ return TopologyStandalone(instance)
+
+
+def test_list(topology):
+ # before creating mapping trees, the backends must exists
+ backendEntry = topology.instance.backend.create(
+ NEW_SUFFIX_1, {BACKEND_NAME: NEW_BACKEND_1})
+
+ ents = topology.instance.mappingtree.list()
+ nb_mappingtree = len(ents)
+
+ # create a first additional mapping tree
+ topology.instance.mappingtree.create(NEW_SUFFIX_1, bename=NEW_BACKEND_1)
+ ents = topology.instance.mappingtree.list()
+ assert len(ents) == (nb_mappingtree + 1)
+
+ suffixes = topology.instance.suffix.list()
+ assert len(suffixes) == 2
+ for suffix in suffixes:
+ topology.instance.log.info("suffix is %s" % suffix)
+
+
+def test_toBackend(topology):
+ backends = topology.instance.suffix.toBackend(suffix=NEW_SUFFIX_1)
+ assert len(backends) == 1
+ backend = backends[0]
+ topology.instance.log.info("backend entry is %r" % backend)
+ assert backend.getValue(
+ BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_SUFFIX]) == NEW_SUFFIX_1
+ assert backend.getValue(
+ BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_NAME]) == NEW_BACKEND_1
+
+
+def test_getParent(topology):
+ parent = topology.instance.suffix.getParent(suffix=NEW_SUFFIX_1)
+ assert parent is None
+
+ backendEntry = topology.instance.backend.create(
+ NEW_CHILDSUFFIX_1, {BACKEND_NAME: NEW_CHILDBACKEND_1})
+ # create a third additional mapping tree, that is child of NEW_SUFFIX_1
+ topology.instance.mappingtree.create(
+ NEW_CHILDSUFFIX_1, bename=NEW_CHILDBACKEND_1, parent=NEW_SUFFIX_1)
+ parent = topology.instance.suffix.getParent(suffix=NEW_CHILDSUFFIX_1)
+ topology.instance.log.info("Retrieved parent of %s: %s" %
(NEW_CHILDSUFFIX_1, parent))
+ assert parent == NEW_SUFFIX_1
+
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -vv %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/test_module_proxy.py
b/src/lib389/lib389/tests/test_module_proxy.py
new file mode 100644
index 0000000..288b9ab
--- /dev/null
+++ b/src/lib389/lib389/tests/test_module_proxy.py
@@ -0,0 +1,79 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import pytest
+import os
+from lib389.ds_instance import DSModuleProxy
+
+
+def test_module_proxy_fun_with_no_arg(fake_ds_class, fake_ds_modules):
+ ds_inst = fake_ds_class()
+ DSModuleProxy.populate_with_proxies(ds_inst, ds_inst,
+ os.path.basename(fake_ds_modules))
+ assert ds_inst.repl.with_no_arg() == ds_inst
+
+
+def test_module_proxy_fun_with_one_arg(fake_ds_class, fake_ds_modules):
+ ds_inst = fake_ds_class()
+ DSModuleProxy.populate_with_proxies(ds_inst, ds_inst,
+ os.path.basename(fake_ds_modules))
+ assert ds_inst.repl.with_one_arg(1) == (ds_inst, 1)
+
+
+def test_module_proxy_plugin_fun_with_no_arg(fake_ds_class, fake_ds_modules):
+ ds_inst = fake_ds_class()
+ DSModuleProxy.populate_with_proxies(ds_inst, ds_inst,
+ os.path.basename(fake_ds_modules))
+ assert ds_inst.plugin.automember.with_no_arg() == ds_inst
+
+
+def test_module_proxy_plugin_fun_with_one_arg(fake_ds_class, fake_ds_modules):
+ ds_inst = fake_ds_class()
+ DSModuleProxy.populate_with_proxies(ds_inst, ds_inst,
+ os.path.basename(fake_ds_modules))
+ assert ds_inst.plugin.automember.with_one_arg(2) == (ds_inst, 2)
+
+
+def test_module_proxy_plugin_call_ds_method(fake_ds_class, fake_ds_modules):
+ ds_inst = fake_ds_class()
+ DSModuleProxy.populate_with_proxies(ds_inst, ds_inst,
+ os.path.basename(fake_ds_modules))
+ assert ds_inst.plugin.automember.calling_ds_method(2) == (ds_inst, 1, 2)
+
+
+def test_module_proxy_plugin_call_another_module(fake_ds_class,
+ fake_ds_modules):
+ ds_inst = fake_ds_class()
+ DSModuleProxy.populate_with_proxies(ds_inst, ds_inst,
+ os.path.basename(fake_ds_modules))
+ assert ds_inst.plugin.automember.calling_repl(2, 3) == 6
+
+
+def test_module_proxy_plugin_call_another_module_kwarg(fake_ds_class,
+ fake_ds_modules):
+ ds_inst = fake_ds_class()
+ DSModuleProxy.populate_with_proxies(ds_inst, ds_inst,
+ os.path.basename
+ (fake_ds_modules))
+ assert ds_inst.plugin.automember.calling_repl_with_kwarg1() == 1
+ assert ds_inst.plugin.automember.calling_repl_with_kwarg2() == 2
+
+
+def test_module_proxy_plugin_call_with_all(fake_ds_class, fake_ds_modules):
+ ds_inst = fake_ds_class()
+ DSModuleProxy.populate_with_proxies(ds_inst, ds_inst,
+ os.path.basename
+ (fake_ds_modules))
+ assert ds_inst.plugin.automember.with_all(0, 3, 2, 1) == (1, 7, 5, 3)
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mo de
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tests/tls_external_test.py
b/src/lib389/lib389/tests/tls_external_test.py
new file mode 100644
index 0000000..68aaa9d
--- /dev/null
+++ b/src/lib389/lib389/tests/tls_external_test.py
@@ -0,0 +1,82 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2017 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+
+import ldap
+
+from lib389.topologies import topology_st
+from lib389.utils import logging
+from lib389.idm.user import UserAccounts
+from lib389._constants import DEFAULT_SUFFIX, SECUREPORT_STANDALONE1
+
+from lib389.config import CertmapLegacy
+
+log = logging.getLogger(__name__)
+
+def test_tls_external(topology_st):
+
+ standalone = topology_st.standalone
+
+ # SETUP TLS
+ standalone.stop()
+ assert(standalone.nss_ssl.reinit() is True)
+ assert(standalone.nss_ssl.create_rsa_ca() is True)
+ assert(standalone.nss_ssl.create_rsa_key_and_cert() is True)
+ # Create a user
+ assert(standalone.nss_ssl.create_rsa_user('testuser') is True)
+ # Now get the details of where the key and crt are.
+ tls_locs = standalone.nss_ssl.get_rsa_user('testuser')
+ # {'ca': ca_path, 'key': key_path, 'crt': crt_path}
+
+ # Start again
+ standalone.start()
+
+ users = UserAccounts(standalone, DEFAULT_SUFFIX)
+ user = users.create(properties={
+ 'uid': 'testuser',
+ 'cn' : 'testuser',
+ 'sn' : 'user',
+ 'uidNumber' : '1000',
+ 'gidNumber' : '2000',
+ 'homeDirectory' : '/home/testuser'
+ })
+
+ standalone.rsa.create()
+ # Set the secure port and nsslapd-security
+ standalone.config.set('nsslapd-secureport', '%s' %
SECUREPORT_STANDALONE1 )
+ standalone.config.set('nsslapd-security', 'on')
+ standalone.sslport = SECUREPORT_STANDALONE1
+ # Now turn on the certmap.
+ cm = CertmapLegacy(standalone)
+ certmaps = cm.list()
+ certmaps['default']['DNComps'] = ''
+ certmaps['default']['FilterComps'] = ['cn']
+ certmaps['default']['VerifyCert'] = 'off'
+ cm.set(certmaps)
+
+ # Check that EXTERNAL is listed in supported mechns.
+ assert(standalone.rootdse.supports_sasl_external())
+ # Restart to allow certmaps to be re-read: Note, we CAN NOT use post_open
+ # here, it breaks on auth. see lib389/__init__.py
+ standalone.restart(post_open=False)
+
+ # Now attempt a bind with TLS external
+ conn = standalone.openConnection(saslmethod='EXTERNAL', connOnly=True,
certdir=standalone.get_cert_dir(), userkey=tls_locs['key'],
usercert=tls_locs['crt'])
+
+ assert(conn.whoami_s() == "dn: uid=testuser,ou=People,dc=example,dc=com")
+
+ # Backup version of the code:
+ # ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
+ # ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, tls_locs['ca'])
+ # ldap.set_option(ldap.OPT_X_TLS_KEYFILE, tls_locs['key'])
+ # ldap.set_option(ldap.OPT_X_TLS_CERTFILE, tls_locs['crt'])
+ # conn = ldap.initialize(standalone.toLDAPURL())
+
+ # sasl_auth = ldap.sasl.external()
+ # conn.sasl_interactive_bind_s("", sasl_auth)
+
diff --git a/src/lib389/lib389/tests/utils_test.py
b/src/lib389/lib389/tests/utils_test.py
new file mode 100644
index 0000000..8104b62
--- /dev/null
+++ b/src/lib389/lib389/tests/utils_test.py
@@ -0,0 +1,139 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import pytest
+from lib389.utils import *
+
+#
+# socket related functions
+#
+import socket
+
+
+def test_normalizeDN():
+ test = [
+ (r'dc=example,dc=com', r'dc=example,dc=com'),
+ (r'dc=example, dc=com', r'dc=example,dc=com'),
+ (r'cn="dc=example,dc=com",cn=config',
+ 'cn=dc\\=example\\,dc\\=com,cn=config'),
+ ]
+ for k, v in test:
+ r = normalizeDN(k)
+ assert r == v, "Mismatch %r vs %r" % (r, v)
+
+
+def test_escapeDNValue():
+ test = [(r'"dc=example, dc=com"', r'\"dc\=example\,\
dc\=com\"')]
+ for k, v in test:
+ r = escapeDNValue(k)
+ assert r == v, "Mismatch %r vs %r" % (r, v)
+
+
+def test_escapeDNFiltValue():
+ test = [(r'"dc=example, dc=com"',
+ '\\22dc\\3dexample\\2c\\20dc\\3dcom\\22')]
+ for k, v in test:
+ r = escapeDNFiltValue(k)
+ assert r == v, "Mismatch %r vs %r" % (r, v)
+
+
+def test_isLocalHost():
+ test = [
+ ('localhost', True),
+ ('localhost.localdomain', True),
+ (socket.gethostname(), True),
+ ('www.google.it', False)]
+ for k, v in test:
+ r = isLocalHost(k)
+ assert r == v, "Mismatch %r vs %r on %r" % (r, v, k)
+
+
+def test_update_newhost_with_fqdn():
+ # This test doesn't account for platform where localhost.localdomain isn't
+ # the name ....
+ test = [
+ ({'hostname': 'localhost'}, (['localhost.localdomain',
'localhost'], True)),
+ ({'hostname': 'remote'}, (['remote'], False)),
+ ]
+ for k, v in test:
+ old = k.copy()
+ expected_host, expected_r = v
+ r = update_newhost_with_fqdn(k)
+ assert expected_r == r, "Mismatch %r vs %r for %r" % (
+ r, expected_r, old)
+ assert k['hostname'] in expected_host, "Mismatch %r vs %r for
%r" % (
+ k['hostname'], expected_host, old)
+
+
+def test_formatInfData():
+ ret = formatInfData({
+ 'hostname': 'localhost.localdomain',
+ 'user-id': 'dirsrv',
+ 'group-id': 'dirsrv',
+ 'ldap-port': 12345,
+ 'root-dn': 'cn=directory manager',
+ 'root-pw': 'password',
+ 'server-id': 'dirsrv',
+ 'suffix': 'o=base1',
+ 'strict_hostname_checking': True,
+ })
+ log.info("content: %r" % ret)
+
+
+def test_formatInfData_withadmin():
+ instance_params = {
+ 'hostname': 'localhost.localdomain',
+ 'user-id': 'dirsrv',
+ 'group-id': 'dirsrv',
+ 'ldap-port': 12346,
+ 'root-dn': 'cn=directory manager',
+ 'root-pw': 'password',
+ 'server-id': 'dirsrv',
+ 'suffix': 'o=base1',
+ 'strict_hostname_checking': True,
+ }
+ admin_params = {
+ 'have_admin': True,
+ 'create_admin': True,
+ 'admin_domain': 'example.com',
+ 'cfgdshost': 'localhost',
+ 'cfgdsport': 12346,
+ 'cfgdsuser': 'admin',
+ 'cfgdspwd': 'admin'}
+ instance_params.update(admin_params)
+ ret = formatInfData(instance_params)
+ log.info("content: %r" % ret)
+
+
+def test_formatInfData_withconfigserver():
+ instance_params = {
+ 'hostname': 'localhost.localdomain',
+ 'user-id': 'dirsrv',
+ 'group-id': 'dirsrv',
+ 'ldap-port': 12346,
+ 'root-dn': 'cn=directory manager',
+ 'root-pw': 'password',
+ 'server-id': 'dirsrv',
+ 'suffix': 'o=base1',
+ 'strict_hostname_checking': True,
+ }
+ admin_params = {
+ 'have_admin': True,
+ 'cfgdshost': 'localhost',
+ 'cfgdsport': 12346,
+ 'cfgdsuser': 'admin',
+ 'cfgdspwd': 'admin',
+ 'admin_domain': 'example.com'}
+ instance_params.update(admin_params)
+ ret = formatInfData(instance_params)
+ log.info("content: %r" % ret)
+
+
+if __name__ == "__main__":
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/tools.py b/src/lib389/lib389/tools.py
new file mode 100644
index 0000000..edb95b1
--- /dev/null
+++ b/src/lib389/lib389/tools.py
@@ -0,0 +1,1008 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""Tools for creating and managing servers
+
+ uses DirSrv
+"""
+import sys
+import os
+import os.path
+import base64
+import select
+import time
+import shutil
+import subprocess
+import tarfile
+import re
+import glob
+import pwd
+import grp
+import logging
+import ldap
+import shlex
+import socket
+import getpass
+
+# from .nss_ssl import nss_create_new_database
+from threading import Timer
+from lib389.paths import Paths
+from lib389._constants import *
+from lib389._ldifconn import LDIFConn
+from lib389.properties import *
+from lib389.utils import (
+ is_a_dn,
+ getcfgdsuserdn,
+ getcfgdsinfo,
+ getcfgdsuserdn,
+ update_newhost_with_fqdn,
+ get_sbin_dir,
+ get_server_user,
+ getdomainname,
+ isLocalHost,
+ formatInfData,
+ getserverroot,
+ update_admin_domain,
+ getadminport,
+ getdefaultsuffix,
+ ensure_bytes,
+ ensure_str,
+ socket_check_open,)
+from lib389.passwd import password_hash, password_generate
+
+# The poc backend api
+from lib389.backend import Backends
+
+try:
+ # There are too many issues with this on EL7
+ # Out of the box, it's just outright broken ...
+ import six.moves.urllib.request
+ import six.moves.urllib.parse
+ import six.moves.urllib.error
+ import six
+except ImportError:
+ pass
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+if MAJOR >= 3:
+ import configparser
+else:
+ import ConfigParser as configparser
+
+__all__ = ['DirSrvTools']
+try:
+ from subprocess import Popen, PIPE, STDOUT
+ HASPOPEN = True
+except ImportError:
+ import popen2
+ HASPOPEN = False
+
+_ds_paths = Paths()
+
+logging.basicConfig(level=logging.DEBUG)
+log = logging.getLogger(__name__)
+
+# Private constants
+PATH_SETUP_DS_ADMIN = CMD_PATH_SETUP_DS_ADMIN
+PATH_SETUP_DS = CMD_PATH_SETUP_DS
+PATH_REMOVE_DS = CMD_PATH_REMOVE_DS
+PATH_ADM_CONF = "/etc/dirsrv/admin-serv/adm.conf"
+GROUPADD = "/usr/sbin/groupadd"
+USERADD = "/usr/sbin/useradd"
+NOLOGIN = "/sbin/nologin"
+
+
+def kill_proc(proc, timeout):
+ """Kill a process after the timeout is reached
+ @param proc - The subprocess process
+ @param timeout - timeout in seconds
+ """
+ timeout["value"] = True
+ proc.kill()
+
+
+def runCmd(cmd, timeout_sec):
+ """Run a system command with a timeout
+ @param cmd - The full system command
+ @param timeout_sec - The timeoput value in seconds
+ @return - The result code
+ """
+ proc = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ timeout = {"value": False}
+ timer = Timer(timeout_sec, kill_proc, [proc, timeout])
+ timer.start()
+ stdout, stderr = proc.communicate()
+ timer.cancel()
+ return proc.returncode
+
+class DirSrvTools(object):
+ """DirSrv mix-in."""
+
+ @staticmethod
+ def cgiFake(sroot, verbose, prog, args):
+ """Run the local program prog as a CGI using the POST
method."""
+ content = urllib.parse.urlencode(args)
+ length = len(content)
+ # setup CGI environment
+ env = os.environ.copy()
+ env['REQUEST_METHOD'] = "POST"
+ env['NETSITE_ROOT'] = sroot
+ env['CONTENT_LENGTH'] = str(length)
+ progdir = os.path.dirname(prog)
+ if HASPOPEN:
+ pipe = Popen(prog, cwd=progdir, env=env,
+ stdin=PIPE, stdout=PIPE, stderr=STDOUT)
+ child_stdin = pipe.stdin
+ child_stdout = pipe.stdout
+ else:
+ saveenv = os.environ
+ os.environ = env
+ child_stdout, child_stdin = popen2.popen2(prog)
+ os.environ = saveenv
+ child_stdin.write(content)
+ child_stdin.close()
+ for line in child_stdout:
+ if verbose:
+ sys.stdout.write(line)
+ ary = line.split(":")
+ if len(ary) > 1 and ary[0] == 'NMC_Status':
+ exitCode = ary[1].strip()
+ break
+ child_stdout.close()
+ if HASPOPEN:
+ osCode = pipe.wait()
+ print("%s returned NMC code %s and OS code %s" %
+ (prog, exitCode, osCode))
+ return exitCode
+
+ @staticmethod
+ def cgiPost(host, port, username, password, uri, verbose, secure,
+ args=None):
+ """Post the request to the admin server.
+
+ Admin server requires authentication, so we use the auth handler
+ classes.
+
+ NOTE: the url classes in python use the deprecated
+ base64.encodestring() function, which truncates lines,
+ causing Apache to give us a 400 Bad Request error for the
+ Authentication string. So, we have to tell
+ base64.encodestring() not to truncate."""
+ args = args or {}
+ prefix = 'http'
+ if secure:
+ prefix = 'https'
+ hostport = host + ":" + port
+ # construct our url
+ url = '%s://%s:%s%s' % (prefix, host, port, uri)
+ # tell base64 not to truncate lines
+ savedbinsize = base64.MAXBINSIZE
+ base64.MAXBINSIZE = 256
+ # create the password manager - we don't care about the realm
+ passman = urllib.request.HTTPPasswordMgrWithDefaultRealm()
+ # add our password
+ passman.add_password(None, hostport, username, password)
+ # create the auth handler
+ authhandler = urllib.request.HTTPBasicAuthHandler(passman)
+ # create our url opener that handles basic auth
+ opener = urllib.request.build_opener(authhandler)
+ # make admin server think we are the console
+ opener.addheaders = [('User-Agent', 'Fedora-Console/1.0')]
+ if verbose:
+ print("requesting url", url)
+ sys.stdout.flush()
+ exitCode = 1
+ try:
+ req = opener.open(url, urllib.parse.urlencode(args))
+ for line in req:
+ if verbose:
+ print (line)
+ ary = line.split(":")
+ if len(ary) > 1 and ary[0] == 'NMC_Status':
+ exitCode = ary[1].strip()
+ break
+ req.close()
+# except IOError, e:
+# print e
+# print e.code
+# print e.headers
+# raise
+ finally:
+ # restore binsize
+ base64.MAXBINSIZE = savedbinsize
+ return exitCode
+
+ @staticmethod
+ def _infoInstanceBackupFS(dirsrv):
+ """
+ Return the information to retrieve the backup file of a given
+ instance
+ It returns:
+ - Directory name containing the backup
+ (e.g. /tmp/slapd-standalone.bck)
+ - The pattern of the backup files
+ (e.g. /tmp/slapd-standalone.bck/backup*.tar.gz)
+ """
+ backup_dir = "%s/slapd-%s.bck" % (dirsrv.backupdir, dirsrv.inst)
+ backup_pattern = os.path.join(backup_dir, "backup*.tar.gz")
+ return backup_dir, backup_pattern
+
+ @staticmethod
+ def clearInstanceBackupFS(dirsrv=None, backup_file=None):
+ """
+ Remove a backup_file or all backup up of a given instance
+ """
+ if backup_file:
+ if os.path.isfile(backup_file):
+ try:
+ os.remove(backup_file)
+ except:
+ log.info("clearInstanceBackupFS: fail to remove %s" %
+ backup_file)
+ pass
+ elif dirsrv:
+ backup_dir, backup_pattern = \
+ DirSrvTools._infoInstanceBackupFS(dirsrv)
+ list_backup_files = glob.glob(backup_pattern)
+ for f in list_backup_files:
+ try:
+ os.remove(f)
+ except:
+ log.info("clearInstanceBackupFS: fail to remove %s" %
+ backup_file)
+ pass
+
+ @staticmethod
+ def checkInstanceBackupFS(dirsrv):
+ """
+ If it exits a backup file, it returns it
+ else it returns None
+ """
+
+ backup_dir, backup_pattern = DirSrvTools._infoInstanceBackupFS(dirsrv)
+ list_backup_files = glob.glob(backup_pattern)
+ if not list_backup_files:
+ return None
+ else:
+ # returns the first found backup
+ return list_backup_files[0]
+
+ @staticmethod
+ def instanceBackupFS(dirsrv):
+ """
+ Saves the files of an instance under
+ /tmp/slapd-<instance_name>.bck/backup_HHMMSS.tar.gz
+ and return the archive file name.
+ If it already exists a such file, it assums it is a valid backup
+ and returns its name
+
+ dirsrv.sroot: root of the instance (e.g. /usr/lib64/dirsrv)
+ dirsrv.inst: instance name (e.g. standalone for
+ /etc/dirsrv/slapd-standalone)
+ dirsrv.confdir: root of the instance config (e.g. /etc/dirsrv)
+ dirsrv.dbdir: directory where is stored the database
+ (e.g. /var/lib/dirsrv/slapd-standalone/db)
+ dirsrv.changelogdir: directory where is stored the changelog
+ (e.g. /var/lib/dirsrv/slapd-master/changelogdb)
+ """
+
+ # First check it if already exists a backup file
+ backup_dir, backup_pattern = DirSrvTools._infoInstanceBackupFS(dirsrv)
+ backup_file = DirSrvTools.checkInstanceBackupFS(dirsrv)
+ if backup_file is None:
+ if not os.path.exists(backup_dir):
+ os.makedirs(backup_dir)
+ else:
+ return backup_file
+
+ # goes under the directory where the DS is deployed
+ listFilesToBackup = []
+ here = os.getcwd()
+ os.chdir(dirsrv.prefix)
+ prefix_pattern = "%s/" % dirsrv.prefix
+
+ # build the list of directories to scan
+ instroot = "%s/slapd-%s" % (dirsrv.sroot, dirsrv.inst)
+ ldir = [instroot]
+ if hasattr(dirsrv, 'confdir'):
+ ldir.append(dirsrv.confdir)
+ if hasattr(dirsrv, 'dbdir'):
+ ldir.append(dirsrv.dbdir)
+ if hasattr(dirsrv, 'changelogdir'):
+ ldir.append(dirsrv.changelogdir)
+ if hasattr(dirsrv, 'errlog'):
+ ldir.append(os.path.dirname(dirsrv.errlog))
+ if hasattr(dirsrv, 'accesslog') and \
+ os.path.dirname(dirsrv.accesslog) not in ldir:
+ ldir.append(os.path.dirname(dirsrv.accesslog))
+
+ # now scan the directory list to find the files to backup
+ for dirToBackup in ldir:
+ for root, dirs, files in os.walk(dirToBackup):
+ for file in files:
+ name = os.path.join(root, file)
+ name = re.sub(prefix_pattern, '', name)
+
+ if os.path.isfile(name):
+ listFilesToBackup.append(name)
+ log.debug("instanceBackupFS add = %s (%s)" %
+ (name, dirsrv.prefix))
+
+ # create the archive
+ name = "backup_%s.tar.gz" % (time.strftime("%m%d%Y_%H%M%S"))
+ backup_file = os.path.join(backup_dir, name)
+ tar = tarfile.open(backup_file, "w:gz")
+
+ for name in listFilesToBackup:
+ if os.path.isfile(name):
+ tar.add(name)
+ tar.close()
+ log.info("instanceBackupFS: archive done : %s" % backup_file)
+
+ # return to the directory where we were
+ os.chdir(here)
+
+ return backup_file
+
+ @staticmethod
+ def instanceRestoreFS(dirsrv, backup_file):
+ """
+ """
+
+ # First check the archive exists
+ if backup_file is None:
+ log.warning("Unable to restore the instance (missing backup)")
+ return 1
+ if not os.path.isfile(backup_file):
+ log.warning("Unable to restore the instance (%s is not a file)" %
+ backup_file)
+ return 1
+
+ #
+ # Second do some clean up
+ #
+
+ # previous db (it may exists new db files not in the backup)
+ log.debug("instanceRestoreFS: remove subtree %s/*" %
os.path.dirname(dirsrv.dbdir))
+ for root, dirs, files in os.walk(os.path.dirname(dirsrv.dbdir)):
+ for d in dirs:
+ if d not in ("bak", "ldif"):
+ log.debug(
+ "instanceRestoreFS: before restore remove directory "
+ "%s/%s" % (root, d))
+ shutil.rmtree("%s/%s" % (root, d))
+
+ # previous error/access logs
+ log.debug("instanceRestoreFS: remove error logs %s" % dirsrv.errlog)
+ for f in glob.glob("%s*" % dirsrv.errlog):
+ log.debug("instanceRestoreFS: before restore remove file %s" %
+ (f))
+ os.remove(f)
+ log.debug("instanceRestoreFS: remove access logs %s" %
+ dirsrv.accesslog)
+ for f in glob.glob("%s*" % dirsrv.accesslog):
+ log.debug("instanceRestoreFS: before restore remove file %s" %
+ (f))
+ os.remove(f)
+
+ # Then restore from the directory where DS was deployed
+ here = os.getcwd()
+ os.chdir(dirsrv.prefix)
+
+ tar = tarfile.open(backup_file)
+ for member in tar.getmembers():
+ if os.path.isfile(member.name):
+ #
+ # restore only writable files
+ # It could be a bad idea and preferably restore all.
+ # Now it will be easy to enhance that function.
+ if os.access(member.name, os.W_OK):
+ log.debug("instanceRestoreFS: restored %s" % member.name)
+ tar.extract(member.name)
+ else:
+ log.debug("instanceRestoreFS: not restored %s "
+ "(no write access)" % member.name)
+ else:
+ log.debug("instanceRestoreFS: restored %s" % member.name)
+ tar.extract(member.name)
+
+ tar.close()
+
+ #
+ # Now be safe, triggers a recovery at restart
+ #
+ guardian_file = os.path.join(dirsrv.dbdir, "guardian")
+ if os.path.isfile(guardian_file):
+ try:
+ log.debug("instanceRestoreFS: remove %s" % guardian_file)
+ os.remove(guardian_file)
+ except:
+ log.warning("instanceRestoreFS: fail to remove %s" %
+ guardian_file)
+ pass
+
+ os.chdir(here)
+
+ @staticmethod
+ def setupSSL(dirsrv, secport=636, sourcedir=None, secargs=None):
+ """configure and setup SSL with a given certificate and
+ restart the server.
+
+ See DirSrv.configSSL for the secargs values
+ """
+ e = dirsrv.configSSL(secport, secargs)
+ log.info("entry is %r" % [e])
+ dn_config = e.dn
+ # get our cert dir
+ e_config = dirsrv.getEntry(
+ dn_config, ldap.SCOPE_BASE, '(objectclass=*)')
+ certdir = e_config.getValue('nsslapd-certdir')
+ # have to stop the server before replacing any security files
+ DirSrvTools.stop(dirsrv)
+ # allow secport for selinux
+ if secport != 636:
+ try:
+ log.debug("Configuring SELinux on port: %s", str(secport))
+
+ subprocess.check_call(["semanage", "port",
"-a", "-t",
+ "ldap_port_t", "-p",
"tcp",
+ str(secport)])
+ except OSError:
+ log.debug("Likely SELinux not supported")
+ pass
+ except subprocess.CalledProcessError:
+ log.debug("SELinux fails to configure")
+ pass
+
+ # eventually copy security files from source dir to our cert dir
+ if sourcedir:
+ for ff in ['cert8.db', 'key3.db', 'secmod.db',
'pin.txt',
+ 'certmap.conf']:
+ srcf = os.path.join(sourcedir, ff)
+ destf = os.path.join(certdir, ff)
+ # make sure dest is writable so we can copy over it
+ try:
+ log.info("Copying security files: %s to %s" %
+ (srcf, destf))
+ mode = os.stat(destf).st_mode
+ newmode = mode | 0o600
+ os.chmod(destf, newmode)
+ except Exception as e:
+ print(e)
+ pass # oh well
+ # copy2 will copy the mode too
+ shutil.copy2(srcf, destf)
+
+ # now, restart the ds
+ DirSrvTools.start(dirsrv, True)
+
+ @staticmethod
+ def runInfProg(prog, content, verbose, prefix=None):
+ """run a program that takes an .inf style file on
stdin"""
+ cmd = [prog]
+ if verbose:
+ cmd.append('-ddd')
+ else:
+ cmd.extend(['-l', '/dev/null'])
+ cmd.extend(['-s', '-f', '-'])
+ log.debug("running: %s " % cmd)
+ if HASPOPEN:
+ pipe = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
+ child_stdin = pipe.stdin
+ child_stdout = pipe.stdout
+ else:
+ pipe = popen2.Popen4(cmd)
+ child_stdin = pipe.tochild
+ child_stdout = pipe.fromchild
+ child_stdin.write(ensure_bytes(content))
+ child_stdin.close()
+ if verbose:
+ log.debug("PID %s" % pipe.pid)
+ while pipe.poll() is None:
+ (rr, wr, xr) = select.select([child_stdout], [], [], 1.0)
+ if rr and len(rr) > 0:
+ line = rr[0].readline()
+ if not line:
+ break
+ if verbose:
+ sys.stdout.write(ensure_str(line))
+ elif verbose:
+ print("timed out waiting to read from pid %s : %s " %
(pipe.pid, cmd))
+ child_stdout.close()
+ exitCode = pipe.wait()
+ # if verbose:
+ log.debug("%s returned exit code %s" % (prog, exitCode))
+ return exitCode
+
+ @staticmethod
+ def removeInstance(dirsrv):
+ """run the remove instance command"""
+ if hasattr(dirsrv, 'prefix'):
+ prefix = dirsrv.prefix
+ else:
+ prefix = None
+
+ prog = os.path.join(_ds_paths.sbin_dir, PATH_REMOVE_DS)
+ try:
+ cmd = [prog, '-i', 'slapd-{}'.format(dirsrv.serverid)]
+ log.info('Running: {}'.format(" ".join(cmd)))
+ subprocess.check_call(cmd)
+ except subprocess.CalledProcessError as e:
+ log.debug('DS has failed to remove the instance {}, '
+ 'output is: {}'.format(dirsrv.serverid, e.output))
+
+ @staticmethod
+ def _offlineDirsrv(args):
+ '''
+ Function to allocate an offline DirSrv instance.
+ This instance is not initialized with the Directory instance
+ (__localinit__() and __add_brookers__() are not called)
+ The properties set are:
+ instance.host
+ instance.port
+ instance.serverid
+ instance.inst
+ instance.prefix
+ instance.backup
+ '''
+ instance = lib389.DirSrv(verbose=True)
+ instance.allocate(args)
+
+ return instance
+
+ @staticmethod
+ def existsBackup(args):
+ '''
+ If the backup of the instance exists, it returns it.
+ Else None
+ '''
+ instance = DirSrvTools._offlineDirsrv(args)
+ return DirSrvTools.checkInstanceBackupFS(instance)
+
+ @staticmethod
+ def existsInstance(args):
+ '''
+ Check if an instance exists.
+ It checks if the following directories/files exist:
+ <confdir>/slapd-<name>
+ <errlog>
+ If it exists it returns a DirSrv instance NOT initialized,
+ else None
+ '''
+ instance = DirSrvTools._offlineDirsrv(args)
+ dirname = _ds_paths.config_dir.format(instance_name=instance.serverid)
+ errorlog = _ds_paths.error_log.format(instance_name=instance.serverid)
+ sroot = os.path.join(instance.prefix, "lib/dirsrv")
+ if os.path.isdir(dirname) and \
+ os.path.isfile(errorlog) and \
+ os.path.isdir(sroot):
+ instance.sroot = sroot
+ instance.errlog = errorlog
+ return instance
+
+ return None
+
+ @staticmethod
+ def createInstance(args, verbose=0):
+ """Create a new instance of directory server and return a
connection
+ to it.
+
+ This function:
+ - guesses the hostname where to create the DS, using
+ localhost by default;
+ - figures out if the given hostname is the local host or not.
+
+ @param args - a dict with the following values {
+ # new instance compulsory values
+ 'newinstance': 'rpolli',
+ 'newsuffix': 'dc=example,dc=com',
+ 'newhost': 'localhost.localdomain',
+ 'newport': 22389,
+ 'newrootpw': 'password',
+
+ # optionally register instance on an admin tree
+ 'have_admin': True,
+
+ # optionally directory where to store instance backup
+ 'backupdir': [ /tmp ]
+
+ # you can configure a new dirsrv-admin
+ 'setup_admin': True,
+
+ # or you need the dirsrv-admin to be already setup
+ 'cfgdshost': 'localhost.localdomain',
+ 'cfgdsport': 22389,
+ 'cfgdsuser': 'admin',
+ 'cfgdspwd': 'admin',
+
+ }
+ """
+ cfgdn = lib389.CFGSUFFIX
+ isLocal = update_newhost_with_fqdn(args)
+
+ # use prefix if binaries are relocated
+ sroot = args.get('sroot', '')
+ prefix = args.setdefault('prefix', '')
+
+ # get the backup directory to store instance backup
+ backupdir = args.get('backupdir', '/tmp')
+
+ # new style - prefix or FHS?
+ args['new_style'] = not args.get('sroot')
+
+ # do we have ds only or ds+admin?
+ if 'no_admin' not in args:
+ ds_admin_path = os.path.join(_ds_paths.sbin_dir, PATH_SETUP_DS_ADMIN)
+ if os.path.isfile(ds_admin_path):
+ args['have_admin'] = True
+
+ # set default values
+ args['have_admin'] = args.get('have_admin', False)
+ args['setup_admin'] = args.get('setup_admin', False)
+
+ # get default values from adm.conf
+ if args['new_style'] and args['have_admin']:
+ admconf = LDIFConn(
+ args['prefix'] + PATH_ADM_CONF)
+ args['admconf'] = admconf.get('')
+
+ # next, get the configuration ds host and port
+ if args['have_admin']:
+ args['cfgdshost'], args['cfgdsport'], cfgdn =
getcfgdsinfo(args)
+ #
+ # if a Config DS is passed, get the userdn. This creates
+ # a connection to the given DS. If you don't want to connect
+ # to this server you should pass 'setup_admin' too.
+ #
+ if args['have_admin'] and not args['setup_admin']:
+ cfgconn = getcfgdsuserdn(cfgdn, args)
+
+ # next, get the server root if not given
+ if not args['new_style']:
+ getserverroot(cfgconn, isLocal, args)
+ # next, get the admin domain
+ if args['have_admin']:
+ update_admin_domain(isLocal, args)
+ # next, get the admin server port and any other information -
+ # close the cfgconn
+ if args['have_admin'] and not args['setup_admin']:
+ asport, secure = getadminport(cfgconn, cfgdn, args)
+ # next, get the posix username
+ get_server_user(args)
+ # fixup and verify other args
+ args['newport'] = args.get('newport', 389)
+ args['newrootdn'] = args.get('newrootdn', DN_DM)
+ args['newsuffix'] = args.get('newsuffix',
+ getdefaultsuffix(args['newhost']))
+
+ if not isLocal or 'cfgdshost' in args:
+ if 'admin_domain' not in args:
+ args['admin_domain'] = getdomainname(args['newhost'])
+ if isLocal and 'cfgdspwd' not in args:
+ args['cfgdspwd'] = "dummy"
+ if isLocal and 'cfgdshost' not in args:
+ args['cfgdshost'] = args['newhost']
+ if isLocal and 'cfgdsport' not in args:
+ args['cfgdsport'] = 55555
+ missing = False
+ for param in ('newhost', 'newport', 'newrootdn',
'newrootpw',
+ 'newinstance', 'newsuffix'):
+ if param not in args:
+ log.error("missing required argument: ", param)
+ missing = True
+ if missing:
+ raise InvalidArgumentError("missing required arguments")
+
+ # try to connect with the given parameters
+ try:
+ newconn = lib389.DirSrv(args['newhost'], args['newport'],
+ args['newrootdn'],
args['newrootpw'],
+ args['newinstance'])
+ newconn.prefix = prefix
+ newconn.backupdir = backupdir
+ newconn.isLocal = isLocal
+ if args['have_admin'] and not args['setup_admin']:
+ newconn.asport = asport
+ newconn.cfgdsuser = args['cfgdsuser']
+ newconn.cfgdspwd = args['cfgdspwd']
+
+ host = args['newhost']
+ port = args['newport']
+ print("Warning: server at %s:%s " % (host, port) +
+ "already exists, returning connection to it")
+ return newconn
+ except ldap.SERVER_DOWN:
+ pass # not running - create new one
+
+ if not isLocal or 'cfgdshost' in args:
+ for param in ('cfgdshost', 'cfgdsport', 'cfgdsuser',
'cfgdspwd',
+ 'admin_domain'):
+ if param not in args:
+ print("missing required argument", param)
+ missing = True
+ if not isLocal and not asport:
+ print("missing required argument admin server port")
+ missing = True
+ if missing:
+ raise InvalidArgumentError("missing required arguments")
+
+ # construct a hash table with our CGI arguments - used with cgiPost
+ # and cgiFake
+ cgiargs = {
+ 'servname': args['newhost'],
+ 'servport': args['newport'],
+ 'rootdn': args['newrootdn'],
+ 'rootpw': args['newrootpw'],
+ 'servid': args['newinstance'],
+ 'suffix': args['newsuffix'],
+ 'servuser': args['newuserid'],
+ 'start_server': 1
+ }
+ if 'cfgdshost' in args:
+ cgiargs['cfg_sspt_uid'] = args['cfgdsuser']
+ cgiargs['cfg_sspt_uid_pw'] = args['cfgdspwd']
+ cgiargs['ldap_url'] = "ldap://%s:%d/%s" % (
+ args['cfgdshost'], args['cfgdsport'], cfgdn)
+ cgiargs['admin_domain'] = args['admin_domain']
+
+ if not isLocal:
+ DirSrvTools.cgiPost(args['newhost'], asport,
args['cfgdsuser'],
+ args['cfgdspwd'],
+ "/slapd/Tasks/Operation/Create",
+ verbose, secure, cgiargs)
+ elif not args['new_style']:
+ prog = sroot + "/bin/slapd/admin/bin/ds_create"
+ if not os.access(prog, os.X_OK):
+ prog = sroot + "/bin/slapd/admin/bin/ds_newinstance"
+ DirSrvTools.cgiFake(sroot, verbose, prog, cgiargs)
+ else:
+ prog = ''
+ if args['have_admin']:
+ prog = os.path.join(_ds_paths.sbin_dir, PATH_SETUP_DS_ADMIN)
+ else:
+ prog = os.path.join(_ds_paths.sbin_dir, PATH_SETUP_DS)
+
+ if not os.path.isfile(prog):
+ log.error("Can't find file: %r, removing extension" %
prog)
+ prog = prog[:-3]
+
+ content = formatInfData(args)
+ DirSrvTools.runInfProg(prog, content, verbose)
+
+ newconn = lib389.DirSrv(args['newhost'], args['newport'],
+ args['newrootdn'], args['newrootpw'],
+ args['newinstance'])
+ newconn.prefix = prefix
+ newconn.backupdir = backupdir
+ newconn.isLocal = isLocal
+ # Now the admin should have been created
+ # but still I should have taken all the required infos
+ # before.
+ if args['have_admin'] and not args['setup_admin']:
+ newconn.asport = asport
+ newconn.cfgdsuser = args['cfgdsuser']
+ newconn.cfgdspwd = args['cfgdspwd']
+ return newconn
+
+ @staticmethod
+ def createAndSetupReplica(createArgs, repArgs):
+ """
+ Pass this sub two dicts - the first one is a dict suitable to create
+ a new instance - see createInstance for more details
+ the second is a dict suitable for replicaSetupAll -
+ see replicaSetupAll
+ """
+ conn = DirSrvTools.createInstance(createArgs)
+ if not conn:
+ print("Error: could not create server", createArgs)
+ return 0
+
+ conn.replicaSetupAll(repArgs)
+ return conn
+
+ @staticmethod
+ def makeGroup(group=DEFAULT_USER):
+ """
+ Create a system group
+ """
+ try:
+ grp.getgrnam(group)
+ print("OK group %s exists" % group)
+ except KeyError:
+ print("Adding group %s" % group)
+ cmd = [GROUPADD, '-r', group]
+ subprocess.Popen(cmd)
+
+ @staticmethod
+ def makeUser(user=DEFAULT_USER, group=DEFAULT_USER, home=DEFAULT_USERHOME):
+ """
+ Create a system user
+ """
+ try:
+ pwd.getpwnam(user)
+ print("OK user %s exists" % user)
+ except KeyError:
+ print("Adding user %s" % user)
+ cmd = [USERADD, '-g', group, '-c', DEFAULT_USER_COMMENT,
'-r',
+ '-d', home, '-s', NOLOGIN, user]
+ subprocess.Popen(cmd)
+
+ @staticmethod
+ def lib389User(user=DEFAULT_USER):
+ """
+ Create the lib389 user/group
+ """
+ DirSrvTools.makeGroup(group=user)
+ time.sleep(1) # Need a little time for the group to get fully created
+ DirSrvTools.makeUser(user=user, group=user, home=DEFAULT_USERHOME)
+
+ @staticmethod
+ def searchHostsFile(expectedHost, ipPattern=None):
+ """
+ Search the hosts file
+ """
+ hostFile = '/etc/hosts'
+
+ with open(hostFile, 'r') as hostfp:
+ # The with statement will automatically close the file after use
+
+ try:
+ for line in hostfp.readlines():
+ if ipPattern is None:
+ words = line.split()
+ if words[1] == expectedHost:
+ return True
+ else:
+ if line.find(ipPattern) >= 0:
+ words = line.split()
+ # We just want to make sure it's in there somewhere
+ if expectedHost in words:
+ return True
+ except AssertionError:
+ raise AssertionError(
+ "Error: %s should contain '%s' host for %s" %
+ ('/etc/hosts', expectedHost, ipPattern))
+ raise AssertionError(
+ "Error: /etc/hosts does not contain '%s' as a host for
%s"
+ % (expectedHost, ipPattern))
+
+ @staticmethod
+ def getLocalhost():
+ """Get the first host value after 127.0.0.1
+ from /etc/hosts file
+ """
+
+ with open('/etc/hosts', 'r') as f:
+ for line in f.readlines():
+ if line.startswith('127.0.0.1'):
+ localhost = line.split()[1]
+ return localhost
+ return None
+
+ @staticmethod
+ def testLocalhost():
+ '''
+ Checks that the 127.0.0.1 is resolved as localhost.localdomain
+ This is required by DSUtil.pm:checkHostname else setup-ds.pl fails
+ '''
+ # One day I'll make this ::1. Ipv6 is the future. The future is now.
+ loopbackIpPattern = '127.0.0.1'
+ DirSrvTools.searchHostsFile(LOCALHOST, loopbackIpPattern)
+ DirSrvTools.searchHostsFile(LOCALHOST_SHORT, loopbackIpPattern)
+
+ @staticmethod
+ def runUpgrade(prefix, online=True):
+ '''
+ Run "setup-ds.pl --update" We simply pass in one DirSrv isntance, and
+ this will update all the instances that are in this prefix. For the
+ update to work we must fix/adjust the permissions of the scripts in:
+
+ /prefix/lib[64]/dirsrv/slapd-INSTANCE/
+ '''
+
+ libdir = os.path.join(_ds_paths.lib_dir, 'dirsrv')
+
+ # Gather all the instances so we can adjust the permissions, otherwise
+ servers = []
+ path = os.path.join(_ds_paths.sysconf_dir, 'dirsrv')
+ for files in os.listdir(path):
+ if files.startswith('slapd-') and not
files.endswith('.removed'):
+ servers.append(os.path.join(libdir, files))
+
+ if len(servers) == 0:
+ # This should not happen
+ log.fatal('runUpgrade: no servers found!')
+ assert False
+
+ '''
+ The setup script calls things like /lib/dirsrv/slapd-instance/db2bak,
+ etc, and when we run the setup perl script it gets permission denied
+ as the default permissions are 750. Adjust the permissions to 755.
+ '''
+ for instance in servers:
+ for files in os.listdir(instance):
+ os.chmod(os.path.join(instance, files), 755)
+
+ # Run the "upgrade"
+ try:
+ prog = os.path.join(_ds_paths.sbin_dir, PATH_SETUP_DS)
+ process = subprocess.Popen([prog, '--update'], shell=False,
+ stdin=subprocess.PIPE)
+ # Answer the interactive questions, as "--update" currently does
+ # not work with INF files
+ process.stdin.write('yes\n')
+ if(online):
+ process.stdin.write('online\n')
+ for x in servers:
+ process.stdin.write(DN_DM + '\n')
+ process.stdin.write(PW_DM + '\n')
+ else:
+ process.stdin.write('offline\n')
+ process.stdin.close()
+ process.wait()
+ if process.returncode != 0:
+ log.fatal('runUpgrade failed! Error: %s ' % process.returncode)
+ assert(False)
+ except:
+ log.fatal('runUpgrade failed!')
+ raise
+
+ @staticmethod
+ def searchFile(filename, pattern):
+ """
+ Search file (or files using a wildcard) for the pattern
+ """
+ found = False
+
+ file_list = glob.glob(filename)
+ for file_name in file_list:
+ try:
+ myfile = open(file_name)
+ for line in myfile:
+ result = re.search(pattern, line)
+ if result:
+ found = True
+ break
+ myfile.close()
+ if found:
+ break
+ except IOError as e:
+ log.error('Problem opening/searching file ' +
+ '(%s): I/O error(%d): %s' %
+ (file_name, e.errno, e.strerror))
+ return found
+
+
+class MockDirSrv(object):
+ """
+ Mock DirSrv Object
+ """
+ host = 'localhost'
+ port = 22389
+ sslport = 0
+
+ def __init__(self, dict_=None):
+ """
+ Init object
+ """
+ if dict_:
+ self.host = dict_['host']
+ self.port = dict_['port']
+ if 'sslport' in dict_:
+ self.sslport = dict_['sslport']
+
+ def __str__(self):
+ """
+ Return ldap URL
+ """
+ if self.sslport:
+ return 'ldaps://%s:%s' % (self.host, self.sslport)
+ else:
+ return 'ldap://%s:%s' % (self.host, self.port)
diff --git a/src/lib389/lib389/topologies.py b/src/lib389/lib389/topologies.py
new file mode 100644
index 0000000..674f1d0
--- /dev/null
+++ b/src/lib389/lib389/topologies.py
@@ -0,0 +1,382 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import os
+import logging
+import time
+
+import pytest
+
+from lib389 import DirSrv
+from lib389.utils import generate_ds_params
+from lib389.replica import Replicas
+from lib389._constants import (args_instance, SER_HOST, SER_PORT, SER_SERVERID_PROP,
SER_CREATION_SUFFIX,
+ ReplicaRole, DEFAULT_SUFFIX, REPLICA_ID)
+
+DEBUGGING = os.getenv('DEBUGGING', default=False)
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+log = logging.getLogger(__name__)
+
+
+def create_topology(topo_dict):
+ """Create a requested topology. Cascading replication scenario
isn't supported
+
+ @param topo_dict - dictionary {ReplicaRole.STANDALONE: num, ReplicaRole.MASTER: num,
+ ReplicaRole.CONSUMER: num}
+ @return - TopologyMain object
+ """
+
+ if not topo_dict:
+ ValueError("You need to specify the dict. For instance:
{ReplicaRole.STANDALONE: 1}")
+
+ if ReplicaRole.HUB in topo_dict.keys():
+ NotImplementedError("Cascading replication scenario isn't
supported."
+ "Please, use existing topology or create your
own.")
+
+ instances = {}
+ ms = {}
+ cs = {}
+ ins = {}
+ replica_dict = {}
+
+ # Create instances
+ for role in topo_dict.keys():
+ for inst_num in range(1, topo_dict[role]+1):
+ instance_data = generate_ds_params(inst_num, role)
+ if DEBUGGING:
+ instance = DirSrv(verbose=True)
+ else:
+ instance = DirSrv(verbose=False)
+ # TODO: Put 'args_instance' to generate_ds_params.
+ # Also, we need to keep in mind that the function returns
+ # SER_SECURE_PORT and REPLICA_ID that are not used in
+ # the instance creation here.
+ args_instance[SER_HOST] = instance_data[SER_HOST]
+ args_instance[SER_PORT] = instance_data[SER_PORT]
+ args_instance[SER_SERVERID_PROP] = instance_data[SER_SERVERID_PROP]
+ args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX
+ args_copied = args_instance.copy()
+ instance.allocate(args_copied)
+ instance_exists = instance.exists()
+ if instance_exists:
+ instance.delete()
+ instance.create()
+ instance.open()
+ if role == ReplicaRole.STANDALONE:
+ ins[instance.serverid] = instance
+ instances.update(ins)
+ if role == ReplicaRole.MASTER:
+ ms[instance.serverid] = instance
+ instances.update(ms)
+ if role == ReplicaRole.CONSUMER:
+ cs[instance.serverid] = instance
+ instances.update(cs)
+ log.info("Instance with parameters {} was
created.".format(args_copied))
+
+ # Set up replication
+ if role in (ReplicaRole.MASTER, ReplicaRole.CONSUMER):
+ replicas = Replicas(instance)
+ replica = replicas.enable(DEFAULT_SUFFIX, role,
instance_data[REPLICA_ID])
+ replica_dict[replica] = instance
+
+ for role_from in topo_dict.keys():
+ # Do not create agreements on consumer
+ if role_from == ReplicaRole.CONSUMER:
+ continue
+
+ # Create agreements: master -> masters, consumers
+ for inst_num_from in range(1, topo_dict[role_from]+1):
+ roles_to = [ReplicaRole.MASTER, ReplicaRole.CONSUMER]
+
+ for role_to in [role for role in topo_dict if role in roles_to]:
+ for inst_num_to in range(1, topo_dict[role_to]+1):
+ # Exclude the instance we created it from
+ if role_from != role_to or inst_num_from != inst_num_to:
+ inst_from_id = "{}{}".format(role_from.name.lower(),
inst_num_from)
+ inst_to_id = "{}{}".format(role_to.name.lower(),
inst_num_to)
+ inst_from = instances[inst_from_id]
+ inst_to = instances[inst_to_id]
+ inst_from.agreement.create(suffix=DEFAULT_SUFFIX,
+ host=inst_to.host,
+ port=inst_to.port)
+
+ # Allow the replicas to get situated with the new agreements
+ if replica_dict:
+ time.sleep(5)
+
+ # Initialize all agreements of one master (consumers)
+ for replica_from, inst_from in replica_dict.items():
+ if replica_from.get_role() == ReplicaRole.MASTER:
+ agmts = inst_from.agreement.list(DEFAULT_SUFFIX)
+ map(lambda agmt: replica_from.start_and_wait(agmt.dn), agmts)
+ break
+
+ # Clear out the tmp dir
+ for instance in instances.values():
+ instance.clearTmpDir(__file__)
+
+ if "standalone1" in instances and len(instances) == 1:
+ return TopologyMain(standalones=instances["standalone1"])
+ else:
+ return TopologyMain(standalones=ins, masters=ms, consumers=cs)
+
+
+class TopologyMain(object):
+ def __init__(self, standalones=None, masters=None, consumers=None, hubs=None):
+ self.all_insts = {}
+
+ if standalones:
+ if isinstance(standalones, dict):
+ self.ins = standalones
+ else:
+ self.standalone = standalones
+ if masters:
+ self.ms = masters
+ self.all_insts.update(self.ms)
+ if consumers:
+ self.cs = consumers
+ self.all_insts.update(self.cs)
+ if hubs:
+ self.hs = hubs
+ self.all_insts.update(self.hs)
+
+ def pause_all_replicas(self):
+ """Pause all agreements in the class instance"""
+
+ for inst in self.all_insts.values():
+ for agreement in inst.agreement.list(suffix=DEFAULT_SUFFIX):
+ inst.agreement.pause(agreement.dn)
+
+ def resume_all_replicas(self):
+ """Resume all agreements in the class instance"""
+
+ for inst in self.all_insts.values():
+ for agreement in inst.agreement.list(suffix=DEFAULT_SUFFIX):
+ inst.agreement.resume(agreement.dn)
+
+
+(a)pytest.fixture(scope="module")
+def topology_st(request):
+ """Create DS standalone instance"""
+
+ topology = create_topology({ReplicaRole.STANDALONE: 1})
+
+ def fin():
+ if DEBUGGING:
+ topology.standalone.stop()
+ else:
+ topology.standalone.delete()
+ request.addfinalizer(fin)
+
+ return topology
+
+
+(a)pytest.fixture(scope="module")
+def topology_i2(request):
+ """Create two instance DS deployment"""
+
+ topology = create_topology({ReplicaRole.STANDALONE: 2})
+
+ def fin():
+ if DEBUGGING:
+ map(lambda inst: inst.stop(), topology.all_insts.values())
+ else:
+ map(lambda inst: inst.delete(), topology.all_insts.values())
+ request.addfinalizer(fin)
+
+ return topology
+
+
+(a)pytest.fixture(scope="module")
+def topology_i3(request):
+ """Create three instance DS deployment"""
+
+ topology = create_topology({ReplicaRole.STANDALONE: 3})
+
+ def fin():
+ if DEBUGGING:
+ map(lambda inst: inst.stop(), topology.all_insts.values())
+ else:
+ map(lambda inst: inst.delete(), topology.all_insts.values())
+ request.addfinalizer(fin)
+
+ return topology
+
+
+(a)pytest.fixture(scope="module")
+def topology_m1c1(request):
+ """Create Replication Deployment with one master and one
consumer"""
+
+ topology = create_topology({ReplicaRole.MASTER: 1,
+ ReplicaRole.CONSUMER: 1})
+ replicas = Replicas(topology.ms["master1"])
+ replicas.test(DEFAULT_SUFFIX, topology.cs["consumer1"])
+
+ def fin():
+ if DEBUGGING:
+ map(lambda inst: inst.stop(), topology.all_insts.values())
+ else:
+ map(lambda inst: inst.delete(), topology.all_insts.values())
+ request.addfinalizer(fin)
+
+ return topology
+
+
+(a)pytest.fixture(scope="module")
+def topology_m2(request):
+ """Create Replication Deployment with two masters"""
+
+ topology = create_topology({ReplicaRole.MASTER: 2})
+ replicas = Replicas(topology.ms["master1"])
+ replicas.test(DEFAULT_SUFFIX, topology.ms["master2"])
+
+ def fin():
+ if DEBUGGING:
+ map(lambda inst: inst.stop(), topology.all_insts.values())
+ else:
+ map(lambda inst: inst.delete(), topology.all_insts.values())
+ request.addfinalizer(fin)
+
+ return topology
+
+
+(a)pytest.fixture(scope="module")
+def topology_m3(request):
+ """Create Replication Deployment with three masters"""
+
+ topology = create_topology({ReplicaRole.MASTER: 3})
+ replicas = Replicas(topology.ms["master1"])
+ replicas.test(DEFAULT_SUFFIX, topology.ms["master3"])
+
+ def fin():
+ if DEBUGGING:
+ map(lambda inst: inst.stop(), topology.all_insts.values())
+ else:
+ map(lambda inst: inst.delete(), topology.all_insts.values())
+ request.addfinalizer(fin)
+
+ return topology
+
+
+(a)pytest.fixture(scope="module")
+def topology_m4(request):
+ """Create Replication Deployment with four masters"""
+
+ topology = create_topology({ReplicaRole.MASTER: 4})
+ replicas = Replicas(topology.ms["master1"])
+ replicas.test(DEFAULT_SUFFIX, topology.ms["master4"])
+
+ def fin():
+ if DEBUGGING:
+ map(lambda inst: inst.stop(), topology.all_insts.values())
+ else:
+ map(lambda inst: inst.delete(), topology.all_insts.values())
+ request.addfinalizer(fin)
+
+ return topology
+
+
+(a)pytest.fixture(scope="module")
+def topology_m2c2(request):
+ """Create Replication Deployment with two masters and two
consumers"""
+
+ topology = create_topology({ReplicaRole.MASTER: 2,
+ ReplicaRole.CONSUMER: 2})
+ replicas = Replicas(topology.ms["master1"])
+ replicas.test(DEFAULT_SUFFIX, topology.cs["consumer1"])
+
+ def fin():
+ if DEBUGGING:
+ map(lambda inst: inst.stop(), topology.all_insts.values())
+ else:
+ map(lambda inst: inst.delete(), topology.all_insts.values())
+ request.addfinalizer(fin)
+
+ return topology
+
+
+(a)pytest.fixture(scope="module")
+def topology_m1h1c1(request):
+ """Create Replication Deployment with one master, one consumer and one
hub"""
+
+ roles = (ReplicaRole.MASTER, ReplicaRole.HUB, ReplicaRole.CONSUMER)
+ instances = []
+ replica_dict = {}
+
+ # Create instances
+ for role in roles:
+ instance_data = generate_ds_params(1, role)
+ if DEBUGGING:
+ instance = DirSrv(verbose=True)
+ else:
+ instance = DirSrv(verbose=False)
+ args_instance[SER_HOST] = instance_data[SER_HOST]
+ args_instance[SER_PORT] = instance_data[SER_PORT]
+ args_instance[SER_SERVERID_PROP] = instance_data[SER_SERVERID_PROP]
+ args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX
+ args_copied = args_instance.copy()
+ instance.allocate(args_copied)
+ instance_exists = instance.exists()
+ if instance_exists:
+ instance.delete()
+ instance.create()
+ instance.open()
+ log.info("Instance with parameters {} was
created.".format(args_copied))
+
+ # Set up replication
+ replicas = Replicas(instance)
+ replica = replicas.enable(DEFAULT_SUFFIX, role, instance_data[REPLICA_ID])
+
+ if role == ReplicaRole.MASTER:
+ master = instance
+ replica_master = replica
+ instances.append(master)
+ if role == ReplicaRole.HUB:
+ hub = instance
+ replica_hub = replica
+ instances.append(hub)
+ if role == ReplicaRole.CONSUMER:
+ consumer = instance
+ instances.append(consumer)
+
+ # Create all the agreements
+ # Creating agreement from master to hub
+ master.agreement.create(suffix=DEFAULT_SUFFIX, host=hub.host, port=hub.port)
+
+ # Creating agreement from hub to consumer
+ hub.agreement.create(suffix=DEFAULT_SUFFIX, host=consumer.host, port=consumer.port)
+
+ # Allow the replicas to get situated with the new agreements...
+ time.sleep(5)
+
+ # Initialize all the agreements
+ agmt = master.agreement.list(DEFAULT_SUFFIX)[0].dn
+ replica_master.start_and_wait(agmt)
+
+ agmt = hub.agreement.list(DEFAULT_SUFFIX)[0].dn
+ replica_hub.start_and_wait(agmt)
+
+ # Check replication is working...
+ replicas = Replicas(master)
+ replicas.test(DEFAULT_SUFFIX, consumer)
+
+ # Clear out the tmp dir
+ master.clearTmpDir(__file__)
+
+ def fin():
+ if DEBUGGING:
+ map(lambda inst: inst.stop(), instances)
+ else:
+ map(lambda inst: inst.delete(), instances)
+ request.addfinalizer(fin)
+
+ return TopologyMain(masters={"master1": master}, hubs={"hub1":
hub}, consumers={"consumer1": consumer})
+
diff --git a/src/lib389/lib389/utils.py b/src/lib389/lib389/utils.py
new file mode 100644
index 0000000..70748a6
--- /dev/null
+++ b/src/lib389/lib389/utils.py
@@ -0,0 +1,873 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+"""Utilities for DirSrv.
+
+ TODO put them in a module!
+"""
+try:
+ from subprocess import Popen as my_popen, PIPE
+except ImportError:
+ from popen2 import popen2
+
+ def my_popen(cmd_l, stdout=None):
+ class MockPopenResult(object):
+ def wait(self):
+ pass
+ p = MockPopenResult()
+ p.stdout, p.stdin = popen2(cmd_l)
+ return p
+
+import re
+import os
+import logging
+import shutil
+import ldap
+import socket
+import time
+import sys
+import filecmp
+from socket import getfqdn
+from ldapurl import LDAPUrl
+from contextlib import closing
+
+import lib389
+from lib389.paths import Paths
+from lib389._constants import (
+ DEFAULT_USER, VALGRIND_WRAPPER, DN_CONFIG, CFGSUFFIX, LOCALHOST,
+ ReplicaRole, CONSUMER_REPLICAID
+ )
+from lib389.properties import (
+ SER_HOST, SER_USER_ID, SER_GROUP_ID, SER_STRICT_HOSTNAME_CHECKING, SER_PORT,
+ SER_ROOT_DN, SER_ROOT_PW, SER_SERVERID_PROP, SER_CREATION_SUFFIX,
+ SER_INST_SCRIPTS_ENABLED, SER_SECURE_PORT, REPLICA_ID
+ )
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+logging.basicConfig(level=logging.DEBUG)
+log = logging.getLogger(__name__)
+#
+# Decorator
+#
+
+
+def static_var(varname, value):
+ def decorate(func):
+ setattr(func, varname, value)
+ return func
+ return decorate
+
+
+#
+# Various searches to be used in getEntry
+# eg getEntry(*searches['NAMINGCONTEXTS'])
+#
+searches = {
+ 'NAMINGCONTEXTS': ('', ldap.SCOPE_BASE, '(objectclass=*)',
+ ['namingcontexts']),
+ 'ZOMBIE': ('', ldap.SCOPE_SUBTREE,
+ '(&(objectclass=glue)(objectclass=extensibleobject))',
['dn'])
+}
+
+#
+# Utilities
+#
+
+
+def is_a_dn(dn, allow_anon=True):
+ """Returns True if the given string is a DN, False
otherwise."""
+ try:
+ if dn == "" and allow_anon is True:
+ # The empty string is anonymous.
+ return True
+ if len(ldap.dn.str2dn(dn)) > 0:
+ # We have valid components in the dn.
+ return True
+ except ldap.DECODING_ERROR:
+ # An invalid dn was given.
+ pass
+ except TypeError:
+ # An invalid type was passed to be checked
+ pass
+ return False
+
+
+def normalizeDN(dn, usespace=False):
+ # not great, but will do until we use a newer version of python-ldap
+ # that has DN utilities
+ ary = ldap.explode_dn(dn.lower())
+ joinstr = ","
+ if usespace:
+ joinstr = ", "
+ return joinstr.join(ary)
+
+
+def escapeDNValue(dn):
+ '''convert special characters in a DN into LDAPv3 escapes.
+
+ e.g.
+ "dc=example,dc=com" -> \"dc\=example\,\
dc\=com\"'''
+ for cc in (' ', '"', '+', ',', ';',
'<', '>', '='):
+ dn = dn.replace(cc, '\\' + cc)
+ return dn
+
+
+def escapeDNFiltValue(dn):
+ '''convert special characters in a DN into LDAPv3 escapes
+ for use in search filters'''
+ for cc in (' ', '"', '+', ',', ';',
'<', '>', '='):
+ dn = dn.replace(cc, '\\%x' % ord(cc))
+ return dn
+
+
+def suffixfilt(suffix):
+ """Return a filter matching any possible suffix form.
+
+ eg. normalized, escaped, spaced...
+ """
+ nsuffix = normalizeDN(suffix)
+ spacesuffix = normalizeDN(nsuffix, True)
+ escapesuffix = escapeDNFiltValue(nsuffix)
+ filt =
('(|(cn=%s)(cn=%s)(cn=%s)(cn="%s")(cn="%s")(cn=%s)(cn="%s"))'
%
+ (escapesuffix, nsuffix, spacesuffix, nsuffix, spacesuffix, suffix,
+ suffix))
+ return filt
+
+
+#
+# path tools
+#
+def get_sbin_dir(sroot=None, prefix=None):
+ """Return the sbin directory (default /usr/sbin)."""
+ if sroot:
+ return "%s/bin/slapd/admin/bin" % sroot
+ elif prefix and prefix != '/':
+ return "%s/sbin" % prefix
+ return "/usr/sbin"
+
+
+def get_bin_dir(sroot=None, prefix=None):
+ """Return the sbin directory (default /usr/bin)."""
+ if sroot:
+ return "%s/bin/slapd/admin/bin" % sroot
+ elif prefix and prefix != '/':
+ return "%s/bin" % prefix
+ return "/usr/bin"
+
+
+def get_data_dir(prefix=None):
+ """Return the shared data directory (default
/usr/share/dirsrv/data)."""
+ if prefix and prefix != '/':
+ return "%s/share/dirsrv/data" % prefix
+ return "/usr/share/dirsrv/data"
+
+
+def get_plugin_dir(prefix=None):
+ """
+ Return the plugin directory (default /usr/lib64/dirsrv/plugins).
+ This should be 64/32bit aware.
+ """
+ if prefix and prefix != '/':
+ # With prefix installations, even 64 bit, it can be under /usr/lib/
+ if os.path.exists("%s/usr/lib/dirsrv/plugins" % prefix):
+ return "%s/usr/lib/dirsrv/plugins" % prefix
+ else:
+ if os.path.exists("%s/usr/lib64/dirsrv/plugins" % prefix):
+ return "%s/usr/lib64/dirsrv/plugins" % prefix
+
+ # Need to check for 32/64 bit installations
+ if not os.path.exists("/usr/lib64/dirsrv/plugins"):
+ if os.path.exists("/usr/lib/dirsrv/plugins"):
+ return "/usr/lib/dirsrv/plugins"
+ return "/usr/lib64/dirsrv/plugins"
+
+
+#
+# valgrind functions
+#
+def valgrind_enable(sbin_dir, wrapper=None):
+ '''
+ Copy the valgrind ns-slapd wrapper into the /sbin directory
+ (making a backup of the original ns-slapd binary).
+
+ The script calling valgrind_enable() must be run as the 'root' user
+ as selinux needs to be disabled for valgrind to work
+
+ The server instance(s) should be stopped prior to calling this function.
+ Then after calling valgrind_enable():
+ - Start the server instance(s) with a timeout of 60 (valgrind takes a
+ while to startup)
+ - Run the tests
+ - Stop the server
+ - Get the results file
+ - Run valgrind_check_file(result_file, "pattern", "pattern",
...)
+ - Run valgrind_disable()
+
+ :param sbin_dir: the location of the ns-slapd binary (e.g. /usr/sbin)
+ :param wrapper: The valgrind wrapper script for ns-slapd (if not set,
+ a default wrapper is used)
+ :raise IOError: If there is a problem setting up the valgrind scripts
+ :raise EnvironmentError: If script is not run as 'root'
+ '''
+
+ if os.geteuid() != 0:
+ log.error('This script must be run as root to use valgrind')
+ raise EnvironmentError
+
+ if not wrapper:
+ # use the default ns-slapd wrapper
+ wrapper = '%s/%s' % (os.path.dirname(os.path.abspath(__file__)),
+ VALGRIND_WRAPPER)
+
+ nsslapd_orig = '%s/ns-slapd' % sbin_dir
+ nsslapd_backup = '%s/ns-slapd.original' % sbin_dir
+
+ if os.path.isfile(nsslapd_backup):
+ # There is a backup which means we never cleaned up from a previous
+ # run(failed test?)
+ if not filecmp.cmp(nsslapd_backup, nsslapd_orig):
+ # Files are different sizes, we assume valgrind is already setup
+ log.info('Valgrind is already enabled.')
+ return
+
+ # Check both nsslapd's exist
+ if not os.path.isfile(wrapper):
+ raise IOError('The valgrind wrapper (%s) does not exist. file=%s' %
+ (wrapper, __file__))
+
+ if not os.path.isfile(nsslapd_orig):
+ raise IOError('The binary (%s) does not exist or is not accessible.' %
+ nsslapd_orig)
+
+ # Make a backup of the original ns-slapd and copy the wrapper into place
+ try:
+ shutil.copy2(nsslapd_orig, nsslapd_backup)
+ except IOError as e:
+ log.fatal('valgrind_enable(): failed to backup ns-slapd, error: %s' %
+ e.strerror)
+ raise IOError('failed to backup ns-slapd, error: %s' % e.strerror)
+
+ # Copy the valgrind wrapper into place
+ try:
+ shutil.copy2(wrapper, nsslapd_orig)
+ except IOError as e:
+ log.fatal('valgrind_enable(): failed to copy valgrind wrapper '
+ 'to ns-slapd, error: %s' % e.strerror)
+ raise IOError('failed to copy valgrind wrapper to ns-slapd, error: %s' %
+ e.strerror)
+
+ # Disable selinux
+ os.system('setenforce 0')
+
+ log.info('Valgrind is now enabled.')
+
+
+def valgrind_disable(sbin_dir):
+ '''
+ Restore the ns-slapd binary to its original state - the server instances
+ are expected to be stopped.
+
+ Note - selinux is enabled at the end of this process.
+
+ :param sbin_dir - the location of the ns-slapd binary (e.g. /usr/sbin)
+ :raise ValueError
+ :raise EnvironmentError: If script is not run as 'root'
+ '''
+
+ if os.geteuid() != 0:
+ log.error('This script must be run as root to use valgrind')
+ raise EnvironmentError
+
+ nsslapd_orig = '%s/ns-slapd' % sbin_dir
+ nsslapd_backup = '%s/ns-slapd.original' % sbin_dir
+
+ # Restore the original ns-slapd
+ try:
+ shutil.copyfile(nsslapd_backup, nsslapd_orig)
+ except IOError as e:
+ log.fatal('valgrind_disable: failed to restore ns-slapd, error: %s' %
+ e.strerror)
+ raise ValueError('failed to restore ns-slapd, error: %s' % e.strerror)
+
+ # Delete the backup now
+ try:
+ os.remove(nsslapd_backup)
+ except OSError as e:
+ log.fatal('valgrind_disable: failed to delete backup ns-slapd, error:'
+ ' %s' % e.strerror)
+ raise ValueError('Failed to delete backup ns-slapd, error: %s' %
+ e.strerror)
+
+ # Enable selinux
+ os.system('setenforce 1')
+
+ log.info('Valgrind is now disabled.')
+
+
+def valgrind_get_results_file(dirsrv_inst):
+ """
+ Return the valgrind results file for the dirsrv instance.
+ """
+
+ """
+ The "ps -ef | grep valgrind" looks like:
+
+ nobody 26239 1 10 14:33 ? 00:00:06 valgrind -q --tool=memcheck
+ --leak-check=yes --leak-resolution=high --num-callers=50
+ --log-file=/var/tmp/slapd.vg.26179 /usr/sbin/ns-slapd.original
+ -D /etc/dirsrv/slapd-localhost -i /var/run/dirsrv/slapd-localhost.pid
+ -w /var/run/dirsrv/slapd-localhost.startpid
+
+ We need to extract the "--log-file" value
+ """
+ cmd = ("ps -ef | grep valgrind | grep 'slapd-" + dirsrv_inst.serverid
+
+ " ' | awk '{ print $14 }' | sed -e
's/\-\-log\-file=//'")
+
+ # Run the command and grab the output
+ p = os.popen(cmd)
+ results_file = p.readline()
+ p.close()
+
+ return results_file
+
+
+def valgrind_check_file(results_file, *patterns):
+ '''
+ Check the valgrind results file for the all the patterns
+ @param result_file - valgrind results file (must be read after server is
+ stopped)
+ @param patterns - A plain text or regex pattern string args that should
+ be searched for
+ @return True/False - Return true if one if the patterns match a stack
+ trace
+ @raise IOError
+ '''
+
+ # Verify results file
+ if not results_file:
+ assert False
+
+ # Check the result file fo the leak text
+ results_file = results_file.replace('\n', '')
+ found = False
+ pattern_count = len(patterns)
+ matched_count = 0
+
+ vlog = open(results_file)
+ for line in vlog:
+ for match_txt in patterns:
+ if re.search(match_txt, line):
+ matched_count += 1
+ break
+
+ if len(line.split()) == 1:
+ # Check if this stack stack matched all the patterns
+ if pattern_count == matched_count:
+ found = True
+ print('valgrind: match found in results file: %s' %
+ (results_file))
+ break
+ else:
+ matched_count = 0
+ vlog.close()
+
+ return found
+
+
+#
+# functions using sockets
+#
+def isLocalHost(host_name):
+ """True if host_name points to a local ip.
+
+ Uses gethostbyname()
+ """
+ # first see if this is a "well known" local hostname
+ if host_name == 'localhost' or \
+ host_name == 'localhost.localdomain' or \
+ host_name == socket.gethostname():
+ return True
+
+ # first lookup ip addr
+ try:
+ ip_addr = socket.gethostbyname(host_name)
+ if ip_addr.startswith("127."):
+ # log.debug("this ip is on loopback, retain only the first octet")
+ ip_addr = '127.'
+ except socket.gaierror:
+ log.debug("no ip address for %r" % host_name)
+ return False
+
+ # next, see if this IP addr is one of our
+ # local addresses
+ p = my_popen(['/sbin/ip', 'addr'], stdout=PIPE)
+ child_stdout = p.stdout.read()
+ found = ('inet %s' % ip_addr).encode() in child_stdout
+ p.wait()
+
+ return found
+
+
+def getdomainname(name=''):
+ fqdn = getfqdn(name)
+ index = fqdn.find('.')
+ if index >= 0:
+ return fqdn[index + 1:]
+ else:
+ # This isn't correct. There is no domain name, so return empty.str
+ # return fqdn
+ return ""
+
+
+def getdefaultsuffix(name=''):
+ dm = getdomainname(name)
+ if dm:
+ return "dc=" + dm.replace('.', ',dc=')
+ else:
+ return 'dc=localdomain'
+
+
+def get_server_user(args):
+ """Return the unix username used from the server inspecting the
following
+ keys in args.
+
+ 'newuserid', 'admconf', 'sroot' -> ssusers.conf
+
+ """
+ if 'newuserid' not in args:
+ if 'admconf' in args:
+ args['newuserid'] = args['admconf'].SuiteSpotUserID
+ elif 'sroot' in args:
+ ssusers = open("%s/shared/config/ssusers.conf" %
args['sroot'])
+ for line in ssusers:
+ ary = line.split()
+ if len(ary) > 1 and ary[0] == 'SuiteSpotUser':
+ args['newuserid'] = ary[-1]
+ ssusers.close()
+ if 'newuserid' not in args:
+ args['newuserid'] = os.environ['LOGNAME']
+ if args['newuserid'] == 'root':
+ args['newuserid'] = DEFAULT_USER
+
+
+def update_newhost_with_fqdn(args):
+ """Replace args[SER_HOST] with its fqdn and returns True if local.
+
+ One of the arguments to createInstance is newhost. If this is specified,
+ we need to convert it to the fqdn. If not given, we need to figure out
+ what the fqdn of the local host is. This method sets newhost in args to
+ the appropriate value and returns True if newhost is the localhost, False
+ otherwise"""
+ if SER_HOST in args:
+ args[SER_HOST] = getfqdn(args[SER_HOST])
+ isLocal = isLocalHost(args[SER_HOST])
+ else:
+ isLocal = True
+ args[SER_HOST] = getfqdn()
+ return isLocal
+
+
+def getcfgdsuserdn(cfgdn, args):
+ """Return a DirSrv object bound anonymously or to the admin user.
+
+ If the config ds user ID was given, not the full DN, we need to figure
+ out the full DN.
+
+ Try in order to:
+ 1- search the directory anonymously;
+ 2- look in ldap.conf;
+ 3- try the default DN.
+
+ This may raise a file or LDAP exception.
+ """
+ # create a connection to the cfg ds
+ conn = lib389.DirSrv(args['cfgdshost'], args['cfgdsport'],
"", "", None)
+ # if the caller gave a password, but not the cfguser DN, look it up
+ if 'cfgdspwd' in args and \
+ ('cfgdsuser' not in args or not is_a_dn(args['cfgdsuser'])):
+ if 'cfgdsuser' in args:
+ ent = conn.getEntry(cfgdn, ldap.SCOPE_SUBTREE,
+ "(uid=%s)" % args['cfgdsuser'],
+ ['dn'])
+ args['cfgdsuser'] = ent.dn
+ elif 'sroot' in args:
+ ldapconf = open(
+ "%s/shared/config/ldap.conf" % args['sroot'],
'r')
+ for line in ldapconf:
+ ary = line.split() # default split is all whitespace
+ if len(ary) > 1 and ary[0] == 'admnm':
+ args['cfgdsuser'] = ary[-1]
+ ldapconf.close()
+ elif 'admconf' in args:
+ args['cfgdsuser'] = args['admconf'].userdn
+ elif 'cfgdsuser' in args:
+ dsuser = ("uid=%s,ou=Administrators,ou=TopologyManagement,%s" %
+ (args['cfgdsuser'], cfgdn))
+ args['cfgdsuser'] = dsuser
+ conn.unbind()
+ conn = lib389.DirSrv(
+ args['cfgdshost'], args['cfgdsport'],
args['cfgdsuser'],
+ args['cfgdspwd'], None)
+ return conn
+
+
+def update_admin_domain(isLocal, args):
+ """Get the admin domain to use."""
+ if isLocal and 'admin_domain' not in args:
+ if 'admconf' in args:
+ args['admin_domain'] = args['admconf'].admindomain
+ elif 'sroot' in args:
+ dsconf = open('%s/shared/config/ds.conf' % args['sroot'],
'r')
+ for line in dsconf:
+ ary = line.split(":")
+ if len(ary) > 1 and ary[0] == 'AdminDomain':
+ args['admin_domain'] = ary[1].strip()
+ dsconf.close()
+
+
+def getoldcfgdsinfo(args):
+ """Use the old style sroot/shared/config/dbswitch.conf to get the
info"""
+ dbswitch = open("%s/shared/config/dbswitch.conf" % args['sroot'],
'r')
+ try:
+ matcher = re.compile(r'^directory\s+default\s+')
+ for line in dbswitch:
+ m = matcher.match(line)
+ if m:
+ url = LDAPUrl(line[m.end():])
+ ary = url.hostport.split(":")
+ if len(ary) < 2:
+ ary.append(389)
+ else:
+ ary[1] = int(ary[1])
+ ary.append(url.dn)
+ return ary
+ finally:
+ dbswitch.close()
+
+
+def getnewcfgdsinfo(new_instance_arguments):
+ """Use the new style prefix /etc/dirsrv/admin-serv/adm.conf.
+
+ new_instance_arguments = {'admconf': obj } where obj.ldapurl != None
+ """
+ try:
+ url = LDAPUrl(new_instance_arguments['admconf'].ldapurl)
+ except AttributeError:
+ log.error("missing ldapurl attribute in new_instance_arguments: %r" %
+ new_instance_arguments)
+ raise
+
+ ary = url.hostport.split(":")
+ if len(ary) < 2:
+ ary.append(389)
+ else:
+ ary[1] = int(ary[1])
+ ary.append(url.dn)
+ return ary
+
+
+def getcfgdsinfo(new_instance_arguments):
+ """Returns a 3-tuple consisting of the host, port, and cfg suffix.
+
+ `new_instance_arguments` = {
+ 'cfgdshost':
+ 'cfgdsport':
+ 'new_style':
+ }
+ We need the host and port of the configuration directory server in order
+ to create an instance. If this was not given, read the dbswitch.conf file
+ to get the information. This method will raise an exception if the file
+ was not found or could not be open. This assumes new_instance_arguments
+ contains the sroot parameter for the server root path.
+ """
+ try:
+ return (new_instance_arguments['cfgdshost'],
+ int(new_instance_arguments['cfgdsport']),
+ CFGSUFFIX)
+ except KeyError: # if keys are missing...
+ if new_instance_arguments['new_style']:
+ return getnewcfgdsinfo(new_instance_arguments)
+
+ return getoldcfgdsinfo(new_instance_arguments)
+
+
+def getserverroot(cfgconn, isLocal, args):
+ """Grab the serverroot from the instance dir of the config ds if the
user
+ did not specify a server root directory"""
+ if cfgconn and 'sroot' not in args and isLocal:
+ ent = cfgconn.getEntry(
+ DN_CONFIG, ldap.SCOPE_BASE, "(objectclass=*)",
+ ['nsslapd-instancedir'])
+ if ent:
+ args['sroot'] = os.path.dirname(
+ ent.getValue('nsslapd-instancedir'))
+
+
+def getadminport(cfgconn, cfgdn, args):
+ """Return a 2-tuple (asport, True) if the admin server is using SSL,
+ False otherwise.
+
+ Get the admin server port so we can contact it via http. We get this from
+ the configuration entry using the CFGSUFFIX and cfgconn. Also get any
+ other information we may need from that entry.
+ """
+ asport = 0
+ secure = False
+ if cfgconn:
+ dn = cfgdn
+ if 'admin_domain' in args:
+ dn = "cn=%s,ou=%s, %s" % (
+ args[SER_HOST], args['admin_domain'], cfgdn)
+ filt = "(&(objectclass=nsAdminServer)(serverHostName=%s)" % args[
+ SER_HOST]
+ if 'sroot' in args:
+ filt += "(serverRoot=%s)" % args['sroot']
+ filt += ")"
+ ent = cfgconn.getEntry(
+ dn, ldap.SCOPE_SUBTREE, filt, ['serverRoot'])
+ if ent:
+ if 'sroot' not in args and ent.serverRoot:
+ args['sroot'] = ent.serverRoot
+ if 'admin_domain' not in args:
+ ary = ldap.explode_dn(ent.dn, 1)
+ args['admin_domain'] = ary[-2]
+ dn = "cn=configuration, " + ent.dn
+ ent = cfgconn.getEntry(dn, ldap.SCOPE_BASE, '(objectclass=*)',
+ ['nsServerPort',
+ 'nsSuiteSpotUser',
+ 'nsServerSecurity'])
+ if ent:
+ asport = ent.nsServerPort
+ secure = (ent.nsServerSecurity and (
+ ent.nsServerSecurity == 'on'))
+ if 'newuserid' not in args:
+ args['newuserid'] = ent.nsSuiteSpotUser
+ cfgconn.unbind()
+ return asport, secure
+
+
+def formatInfData(args):
+ """Return the .inf data for a silence setup via setup-ds.pl.
+
+ args = {
+ # new instance values
+ newhost, newuserid, newport, SER_ROOT_DN, newrootpw, newsuffix,
+
+ # The following parameters require to register the new instance
+ # in the admin server
+ have_admin, cfgdshost, cfgdsport, cfgdsuser,cfgdspwd, admin_domain
+
+ InstallLdifFile, AddOrgEntries, ConfigFile, SchemaFile,
+ ldapifilepath
+
+ # Setup the o=NetscapeRoot namingContext
+ setup_admin,
+ }
+
+ @see
https://access.redhat.com/site/documentation/en-US/
+ Red_Hat_Directory_Server/8.2/html/Installation_Guide/
+ Installation_Guide-Advanced_Configuration-Silent.html
+ [General]
+ FullMachineName=
dir.example.com
+ SuiteSpotUserID= nobody
+ SuiteSpotGroup= nobody
+ AdminDomain=
example.com
+ ConfigDirectoryAdminID= admin
+ ConfigDirectoryAdminPwd= admin
+ ConfigDirectoryLdapURL= ldap://dir.example.com:389/o=NetscapeRoot
+
+ [slapd]
+ SlapdConfigForMC= Yes
+ UseExistingMC= 0
+ ServerPort= 389
+ ServerIdentifier= dir
+ Suffix= dc=example,dc=com
+ RootDN= cn=Directory Manager
+ RootDNPwd= password
+ ds_bename=exampleDB
+ AddSampleEntries= No
+
+ [admin]
+ Port= 9830
+ ServerIpAddress= 111.11.11.11
+ ServerAdminID= admin
+ ServerAdminPwd= admin
+
+ """
+ args = args.copy()
+ args['CFGSUFFIX'] = CFGSUFFIX
+
+ content = ("[General]" "\n")
+ content += ("FullMachineName= %s\n" % args[SER_HOST])
+ content += ("SuiteSpotUserID= %s\n" % args[SER_USER_ID])
+ content += ("nSuiteSpotGroup= %s\n" % args[SER_GROUP_ID])
+ content += ("StrictHostCheck= %s\n" % args[SER_STRICT_HOSTNAME_CHECKING])
+
+ if args.get('have_admin'):
+ content += ("AdminDomain= %(admin_domain)s" "\n"
+ "ConfigDirectoryLdapURL= ldap://%(cfgdshost)s:%"
+ "(cfgdsport)d/%(CFGSUFFIX)s" "\n"
+ "ConfigDirectoryAdminID= %(cfgdsuser)s" "\n"
+ "ConfigDirectoryAdminPwd= %(cfgdspwd)s" "\n") %
args
+
+ content += ("\n" "\n" "[slapd]" "\n")
+ content += ("ServerPort= %s\n" % args[SER_PORT])
+ content += ("RootDN= %s\n" % args[SER_ROOT_DN])
+ content += ("RootDNPwd= %s\n" % args[SER_ROOT_PW])
+ content += ("ServerIdentifier= %s\n" % args[SER_SERVERID_PROP])
+ content += ("Suffix= %s\n" % args[SER_CREATION_SUFFIX])
+
+ # Create admin?
+ if args.get('setup_admin'):
+ content += ("SlapdConfigForMC= Yes" "\n"
+ "UseExistingMC= 0 " "\n")
+
+ if 'InstallLdifFile' in args:
+ content += """\nInstallLdifFile= %s\n""" %
args['InstallLdifFile']
+ if 'AddOrgEntries' in args:
+ content += """\nAddOrgEntries= %s\n""" %
args['AddOrgEntries']
+ if 'ConfigFile' in args:
+ for ff in args['ConfigFile']:
+ content += """\nConfigFile= %s\n""" % ff
+ if 'SchemaFile' in args:
+ for ff in args['SchemaFile']:
+ content += """\nSchemaFile= %s\n""" % ff
+
+ if 'ldapifilepath' in args:
+ content += "\nldapifilepath=%s\n" % args['ldapifilepath']
+ if SER_INST_SCRIPTS_ENABLED in args:
+ content += "\n{}={}\n".format(SER_INST_SCRIPTS_ENABLED,
args[SER_INST_SCRIPTS_ENABLED])
+
+ return content
+
+
+def generate_ds_params(inst_num, role=ReplicaRole.STANDALONE):
+ """Generate a host, port, secure port, server ID and replica ID
+ for the selected role and instance number. I.e. inst_num=1, role="master".
+
+ @param inst_num - an instance number in a range from 1 to 99
+ @param role - ReplicaRole.STANDALONE, ReplicaRole.MASTER, ReplicaRole.HUB,
ReplicaRole.CONSUMER
+ @return - the dict with next keys: host, port, secure port, server id and replica id
+ """
+
+ if inst_num not in range(1, 100):
+ raise ValueError("Instance number should be in a range from 1 to 99")
+
+ if role not in (ReplicaRole.STANDALONE, ReplicaRole.MASTER, ReplicaRole.HUB,
ReplicaRole.CONSUMER):
+ raise ValueError('Role should be {}, {}, {},
{}'.format(ReplicaRole.STANDALONE, ReplicaRole.MASTER,
+ ReplicaRole.HUB,
ReplicaRole.CONSUMER))
+
+ instance_data = {}
+ relevant_num = 38900
+
+ # Set relevant number for ports
+ if role == ReplicaRole.MASTER:
+ relevant_num += 100
+ elif role == ReplicaRole.HUB:
+ relevant_num += 200
+ elif role == ReplicaRole.CONSUMER:
+ relevant_num += 300
+
+ # Define replica ID
+ if role == ReplicaRole.MASTER:
+ replica_id = inst_num
+ else:
+ replica_id = CONSUMER_REPLICAID
+
+ # Fill the dict with data
+ server_id = "{}{}".format(role.name.lower(), inst_num)
+ instance_data[SER_HOST] = LOCALHOST
+ instance_data[SER_PORT] = relevant_num + inst_num
+ instance_data[SER_SECURE_PORT] = relevant_num + inst_num + 24700
+ instance_data[SER_SERVERID_PROP] = server_id
+ instance_data[REPLICA_ID] = replica_id
+
+ return instance_data
+
+
+def get_ds_version():
+ """
+ Return version of ns-slapd installed on this system. This is determined by the
defaults.inf
+ file, so is correct for the built and installed ns-slapd binary. This function works
without
+ root permissions.
+
+ returns a string of the form: 1.3.4.8
+ """
+ p = Paths()
+ return p.version
+
+
+def ds_is_older(ver):
+ """Return True if current version of ns-slapd is older than provided
+ version"""
+ return get_ds_version() < ver
+
+
+def ds_is_newer(ver):
+ return get_ds_version() >= ver
+
+
+def getDateTime():
+ """
+ Return the date and time:
+
+ 2016-04-21 21:21:00
+
+ """
+ return time.strftime("%Y-%m-%d %H:%M:%S")
+
+
+def socket_check_open(host, port):
+ with closing(socket.socket(socket.AF_INET6, socket.SOCK_STREAM)) as sock:
+ if sock.connect_ex((host, port)) == 0:
+ return True
+ else:
+ return False
+
+
+def ensure_bytes(val):
+ if val != None and type(val) != bytes:
+ return val.encode()
+ return val
+
+
+def ensure_str(val):
+ if val != None and type(val) != str:
+ return val.decode('utf-8')
+ return val
+
+
+def ensure_int(val):
+ if val is not None and not isinstance(val, int):
+ return int(val)
+ return val
+
+
+def ensure_list_bytes(val):
+ return [ensure_bytes(v) for v in val]
+
+
+def ensure_list_str(val):
+ return [ensure_str(v) for v in val]
+
+
+def ensure_list_int(val):
+ return [ensure_int(v) for v in val]
+
+
+def ensure_dict_str(val):
+ if MAJOR <= 2:
+ return val
+ retdict = {}
+ for k in val:
+ if isinstance(val[k], list):
+ retdict[k] = ensure_list_str(val[k])
+ else:
+ retdict[k] = ensure_str(val[k])
+ return retdict
diff --git a/src/lib389/python-lib389.spec b/src/lib389/python-lib389.spec
new file mode 100644
index 0000000..88901c9
--- /dev/null
+++ b/src/lib389/python-lib389.spec
@@ -0,0 +1,239 @@
+%global srcname lib389
+%global sum A library for accessing, testing, and configuring the 389 Directory Server
+%global vers 1.0.4
+
+Name: python-%{srcname}
+Summary:%{sum}
+Version: %{vers}
+# RESET THIS TO 0 ON VERSION CHANGE
+Release: 4%{?dist}
+%global tarver %{version}-1
+Source0:
http://www.port389.org/binaries/%{name}-%{tarver}.tar.bz2
+License: GPLv3+
+Group: Development/Libraries
+BuildArch: noarch
+Url:
http://port389.org/docs/389ds/FAQ/upstream-test-framework.html
+%if 0%{?rhel} >= 8 || 0%{?fedora}
+BuildRequires: python2-devel
+BuildRequires: python2-setuptools
+BuildRequires: python%{python3_pkgversion}-devel
+BuildRequires: python%{python3_pkgversion}-setuptools
+%else
+BuildRequires: python-devel
+BuildRequires: python-setuptools
+%endif
+%description
+This module contains tools and libraries for accessing, testing,
+and configuring the 389 Directory Server.
+
+
+%package -n python2-%{srcname}
+Summary: %{sum}
+Requires: python-ldap
+Requires: krb5-workstation
+Requires: krb5-server
+Requires: openssl
+Requires: iproute
+Requires: python-enum34
+# Conditional will need to change later.
+%if 0%{?rhel} >= 8 || 0%{?fedora}
+Requires: python2
+Requires: python2-pytest
+Requires: python2-six
+Requires: python2-pyasn1
+Requires: python2-pyasn1-modules
+Requires: python2-dateutil
+%else
+Requires: python
+Requires: pytest
+Requires: python-six
+Requires: python-pyasn1
+Requires: python-pyasn1-modules
+Requires: python-dateutil
+%endif
+%{?python_provide:%python_provide python2-%{srcname}}
+%description -n python2-%{srcname}
+This module contains tools and libraries for accessing, testing,
+and configuring the 389 Directory Server.
+
+# Can't build on EL7! Python3 tooling is too broken :(
+# We have to use >= 8, because <= 7 doesn't work ....
+%if 0%{?rhel} >= 8 || 0%{?fedora}
+%package -n python%{python3_pkgversion}-%{srcname}
+Summary: %{sum}
+Requires: krb5-workstation
+Requires: krb5-server
+Requires: openssl
+Requires: iproute
+Requires: python%{python3_pkgversion}
+Requires: python%{python3_pkgversion}-pytest
+Requires: python%{python3_pkgversion}-pyldap
+Requires: python%{python3_pkgversion}-six
+Requires: python%{python3_pkgversion}-pyasn1
+Requires: python%{python3_pkgversion}-pyasn1-modules
+Requires: python%{python3_pkgversion}-dateutil
+%{?python_provide:%python_provide python%{python3_pkgversion}-%{srcname}}
+%description -n python%{python3_pkgversion}-%{srcname}
+This module contains tools and libraries for accessing, testing,
+and configuring the 389 Directory Server.
+%endif
+
+%prep
+%autosetup -n %{name}-%{tarver}
+
+%build
+# JFC you need epel only devel packages for this, python 3 is the worst
+%if 0%{?rhel} >= 8 || 0%{?fedora}
+%py2_build
+%py3_build
+%else
+%{__python} setup.py build
+%endif
+
+%install
+%if 0%{?rhel} >= 8 || 0%{?fedora}
+%py2_install
+%py3_install
+%else
+%{__python} setup.py install -O1 --skip-build --root %{buildroot}
+%endif
+
+%files -n python2-%{srcname}
+%license LICENSE
+%doc README.md
+%{python2_sitelib}/*
+%if 0%{?rhel} >= 8 || 0%{?fedora}
+%exclude %{_sbindir}/*
+%else
+%{_sbindir}/*
+%endif
+
+%if 0%{?rhel} >= 8 || 0%{?fedora}
+%files -n python%{python3_pkgversion}-%{srcname}
+%license LICENSE
+%doc README.md
+%{python3_sitelib}/*
+%{_sbindir}/*
+%endif
+
+%changelog
+* Thu Mar 9 2017 William Brown <wibrown(a)redhat.com> - 1.0.4-1
+- Bump version to 1.0.4
+- Ticket 4 - Cert detection breaks some tests
+- Ticket 49137 - Add sasl plain tests, lib389 support
+- Ticket 2 - pytest mark with version relies on root
+- Ticket 49126 - DIT management tool
+- dbscan - Support additional options (-t truncate -R)
+- Ticket 49101 - Python 2 generate example entries
+- Ticket 49103 - python 2 support for installer
+- Fixed regression with offline db2ldif
+- Ticket 47747 - Add topology_i2 and topology_i3
+- Ticket 49087 - lib389 resolve jenkins issues
+- Ticket 48413 - Improvements to lib389 for rest
+- Ticket 49083 - Support prefix for discovery of the defaults.inf file.
+- Ticket 49055 - Fix debugging mode issue
+- Ticket 49060 - Increase number of masters, hubs and consumers in topology
+- Ticket 47747 - Add more topology fixtures
+- Ticket 47840 - Add InstScriptsEnabled argument
+- Ticket 47747 - Add topology fixtures module
+- Ticket 48707 - Implement draft-wibrown-ldapssotoken-01
+- Ticket 49022 - Lib389, py3 installer cannot create entries in backend
+- Ticket 49024 - Fix paths to the dbdir parent
+- Ticket 49024 - Fix db_dir paths
+- Ticket 49024 - Fix paths in tools module
+- Ticket 48961 - Fix lib389 minor issues shown by 48961 test
+- Fix runUpgrade tool issues
+- Ticket 49010 - Lib389 fails to start with systemctl changes
+- Ticket 49007 - lib389 fixes for paths to use online values
+- Ticket 49005 - Update lib389 to work in containers correctly.
+- Ticket 48991 - Fix lib389 spec for python2 and python3
+- Ticket 48984 - Add lib389 paths module
+- Ticket 48951 - dsadm dsconfig status and plugin
+- Ticket 47957 - Update the replication "idle" status string
+- Ticket 48951 - dsadm and dsconf base files
+
+* Thu Sep 22 2016 William Brown <wibrown(a)redhat.com> - 1.0.3-1
+- Bump version to 1.0.3 pre-release
+- Ticket 48952 - Restart command needs a sleep
+- Ticket 47957 - Update the replication "idle" status string
+- Ticket 48949 - Fix ups for style and correctness
+- Ticket 48951 - dsadm and dsconf base files
+- Ticket 48951 - dsadm dsconfig status and plugin
+- Ticket 48984 - Add lib389 paths module
+- Ticket 48991 - Fix lib389 spec for python2 and python3
+- Ticket 48949 - configparser fallback not python2 compatible
+- Ticket 48949 - os.makedirs() exist_ok not python2 compatible, added try/except
+- Ticket 48949 - change default file path generation - use os.path.join
+- Ticket 48949 - added copying slapd-collations.conf
+
+
+* Mon Aug 1 2016 Mark Reynolds <mreynolds(a)redhat.com> - 1.0.2-1
+- Bump version to 1.0.2
+- Ticket 48946 - openConnection should not fully popluate DirSrv object
+- Ticket 48832 - Add DirSrvTools.getLocalhost() function
+- Ticket 48382 - Fix serverCmd to get sbin dir properly
+- Bug 1347760 - Information disclosure via repeated use of LDAP ADD operation, etc.
+- Ticket 48937 - Cleanup valgrind wrapper script
+- Ticket 48923 - Fix additional issue with serverCmd
+- Ticket 48923 - serverCmd timeout not working as expected
+- Ticket 48917 - Attribute presence
+- Ticket 48911 - Plugin improvements for lib389
+- Ticket 48911 - Improve plugin support based on new mapped objects
+- Ticket 48910 - Fixes for backend tests and lib389 reliability.
+- Ticket 48860 - Add replication tools
+- Ticket 48888 - Correction to create of dsldapobject
+- Ticket 48886 - Fix NSS SSL library in lib389
+- Ticket 48885 - Fix spec file requires
+- Ticket 48884 - Bugfixes for mapped object and new connections
+- Ticket 48878 - better style for backend in backend_test.py
+- Ticket 48878 - pep8 fixes part 2
+- Ticket 48878 - pep8 fixes and fix rpm to build
+- Ticket 48853 - Prerelease installer
+- Ticket 48820 - Begin to test compatability with py.test3, and the new orm
+- Ticket 48434 - Fix for negative tz offsets
+- Ticket 48857 - Remove python-krbV from lib389
+- Ticket 48820 - Move Encryption and RSA to the new object types
+- Ticket 48431 - lib389 integrate ldclt
+- Ticket 48434 - lib389 logging tools
+- Ticket 48796 - add function to remove logs
+- Ticket 48771 - lib389 - get ns-slapd version
+- Ticket 48830 - Convert lib389 to ip route tools
+- Ticket 48763 - backup should run regardless of existing backups.
+- Ticket 48434 - lib389 logging tools
+- Ticket 48798 - EL6 compat for lib389 tests for DH params
+- Ticket 48798 - lib389 add ability to create nss ca and certificate
+- Ticket 48433 - Aci linting tools
+- Ticket 48791 - format args in server tools
+- Ticket 48399 - Helper makefile is missing mkdir dist
+- Ticket 48399 - Helper makefile is missing mkdir dist
+- Ticket 48794 - lib389 build requires are on a single line
+- Ticket 48660 - Add function to convert binary values in an entry to base64
+- Ticket 48764 - Fix mit krb password to be random.
+- Ticket 48765 - Change default ports for standalone topology
+- Ticket 48750 - Clean up logging to improve command experience
+- Ticket 48751 - Improve lib389 ldapi support
+- Ticket 48399 - Add helper makefile to lib389 to build and install
+- Ticket 48661 - Agreement test suite fails at the test_changes case
+- Ticket 48407 - Add test coverage module for lib389 repo
+- Ticket 48357 - clitools should standarise their args
+- Ticket 48560 - Make verbose handling consistent
+- Ticket 48419 - getadminport() should not a be a static method
+- Ticket 48415 - Add default domain parameter
+- Ticket 48408 - RFE escaped default suffix for tests
+- Ticket 48405 - python-lib389 in rawhide is missing dependencies
+- Ticket 48401 - Revert typecheck
+- Ticket 48401 - lib389 Entry hasAttr returs dict instead of false
+- Ticket 48390 - RFE Improvements to lib389 monitor features for rest389
+- Ticket 48358 - Add new spec file
+- Ticket 48371 - weaker host check on localhost.localdomain
+
+* Mon Dec 7 2015 Mark Reynolds <mreynolds(a)redhat.com> - 1.0.1-1
+- Removed downloaded dependencies, and added python_provide macro
+- Fixed Source0 URL in spec file
+
+* Fri Dec 4 2015 Mark Reynolds <mreynolds(a)redhat.com> - 1.0.1-1
+- Renamed package to python-lib389, and simplified the spec file
+
+* Tue Dec 1 2015 Mark Reynolds <mreynolds(a)redhat.com> - 1.0.1-1
+- Bugzilla 1287846 - Submit lib389 python module to access the 389 DS
+
diff --git a/src/lib389/requirements.txt b/src/lib389/requirements.txt
new file mode 100644
index 0000000..f208938
--- /dev/null
+++ b/src/lib389/requirements.txt
@@ -0,0 +1,9 @@
+python-ldap
+wheel
+nose
+pyasn1
+pyasn1-modules
+six
+pytest
+python-dateutil
+enum34
diff --git a/src/lib389/setup.cfg b/src/lib389/setup.cfg
new file mode 100644
index 0000000..b498205
--- /dev/null
+++ b/src/lib389/setup.cfg
@@ -0,0 +1,3 @@
+[bdist_rpm]
+requires=python-ldap
+
diff --git a/src/lib389/setup.py b/src/lib389/setup.py
new file mode 100644
index 0000000..2053c98
--- /dev/null
+++ b/src/lib389/setup.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2015 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+#
+# A setup.py file
+#
+
+from setuptools import setup, find_packages
+from os import path
+
+here = path.abspath(path.dirname(__file__))
+
+# fedora/rhel versioning or PEP440?; ATM semantic versioning
+# with open(path.join(here, 'VERSION'), 'r') as version_file:
+# version = version_file.read().strip()
+
+version = "1.4.0.1"
+
+with open(path.join(here, 'README.md'), 'r') as f:
+ long_description = f.read()
+
+setup(
+ name='lib389',
+ license='GPLv3+',
+ version=version,
+ description='A library for accessing and configuring the 389 Directory ' +
+ 'Server',
+ long_description=long_description,
+
url='http://port389.org/wiki/Upstream_test_framework';,
+
+ author='Red Hat Inc.',
+ author_email='389-devel(a)lists.fedoraproject.org',
+
+ classifiers=[
+ 'Development Status :: 4 - Beta',
+ 'Intended Audience :: Developers',
+ 'Operating System :: POSIX :: Linux',
+ 'Programming Language :: Python :: 2 :: Only',
+ 'Programming Language :: Python :: 2.7',
+ 'Topic :: Software Development :: Libraries',
+ 'Topic :: Software Development :: Quality Assurance',
+ 'Topic :: Software Development :: Testing'],
+
+ keywords='389 directory server test configure',
+ packages=find_packages(exclude=['tests*']),
+
+ # find lib389/clitools -name ds\* -exec echo \''{}'\', \;
+ data_files=[
+ ('/usr/sbin/', [
+ # 'lib389/clitools/ds_setup',
+ 'cli/dsctl',
+ 'cli/dsconf',
+ 'cli/dscreate',
+ 'cli/dsidm',
+ ]),
+ ],
+
+ install_requires=['pyldap'],
+)
diff --git a/src/lib389/tox.ini b/src/lib389/tox.ini
new file mode 100644
index 0000000..23c3b5c
--- /dev/null
+++ b/src/lib389/tox.ini
@@ -0,0 +1,6 @@
+[tox]
+envlist = py27, py34
+
+[testenv]
+deps = -rrequirements.txt
+commands = py.test
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.