From 1e644fcbc93670fdc306141fd138903d90f7f295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B0=D0=BC=D0=BE=D1=83=D0=BA=D0=B8=D0=BD=20=D0=90?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9?= Date: Mon, 11 Oct 2010 15:44:19 +0400 Subject: [PATCH] The initial project files. --- LICENCE | 202 +++++++++++++++ README | 15 ++ i18n/cl_ldap_ru.mo | Bin 0 -> 128686 bytes ldif/base.ldif | 31 +++ pym/__init__.py | 0 pym/cl_fill_ldap.py | 81 ++++++ pym/cl_ldap_api.py | 184 ++++++++++++++ pym/cl_ldap_service.py | 239 ++++++++++++++++++ pym/cl_ldap_setup_cmd.py | 108 ++++++++ pym/cl_share_cmd.py | 78 ++++++ pym/cl_vars_ldap.py | 77 ++++++ scripts/cl-ldap-setup | 65 +++++ setup.cfg | 5 + setup.py | 89 +++++++ templates/.calculate_directory | 1 + templates/setup/.calculate_directory | 1 + templates/setup/openldap/.calculate_directory | 2 + .../openldap/step-1/.calculate_directory | 2 + templates/setup/openldap/step-1/slapd.conf | 60 +++++ .../openldap/step-2/.calculate_directory | 2 + templates/setup/openldap/step-2/slapd.conf | 3 + 21 files changed, 1245 insertions(+) create mode 100644 LICENCE create mode 100644 README create mode 100644 i18n/cl_ldap_ru.mo create mode 100644 ldif/base.ldif create mode 100644 pym/__init__.py create mode 100644 pym/cl_fill_ldap.py create mode 100644 pym/cl_ldap_api.py create mode 100644 pym/cl_ldap_service.py create mode 100644 pym/cl_ldap_setup_cmd.py create mode 100644 pym/cl_share_cmd.py create mode 100644 pym/cl_vars_ldap.py create mode 100644 scripts/cl-ldap-setup create mode 100644 setup.cfg create mode 100755 setup.py create mode 100644 templates/.calculate_directory create mode 100644 templates/setup/.calculate_directory create mode 100644 templates/setup/openldap/.calculate_directory create mode 100644 templates/setup/openldap/step-1/.calculate_directory create mode 100644 templates/setup/openldap/step-1/slapd.conf create mode 100644 templates/setup/openldap/step-2/.calculate_directory create mode 100644 templates/setup/openldap/step-2/slapd.conf diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENCE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README b/README new file mode 100644 index 0000000..6c9dcef --- /dev/null +++ b/README @@ -0,0 +1,15 @@ +AUTHOR: Calculate Ltd. + +INSTALL +------- + +calculate-server needs the following library version installed, in order to run: + python >= 2.5 + python-ldap >= 2.0.0 + pyxml >= 0.8 + calculate-lib >= 2.2.0.0 + +To install calculate-ldap, just execute the install script 'setup.py'. +Example: + + ./setup.py install diff --git a/i18n/cl_ldap_ru.mo b/i18n/cl_ldap_ru.mo new file mode 100644 index 0000000000000000000000000000000000000000..8d12e9e7fe1689513835068be93239448822e67c GIT binary patch literal 128686 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 VD|bebVQa~wrX1C3AiF(^^&F$S z@L!GbHr~OTPk!87Et+yvuIsh6B4BW(jphbntk~O6>BdoQlpcnJn)tsxo#Pe(Xp*n_ zXXl?ee{tirEm>tqStepX(*ad$1`vQUoE}}I)XnQ*e1s6f1$(hea)o#8OW9$;Mz$RS z0*&Zna@T9q*R<^rNYh>9@m9_d;4)df5*_z9f0;k}>_Spokn)aydF{sC*Vr{=QTEc6 z5;@(H$<@RNbnAy|W3B`+h64?N9f?m$)@N2Rp635Rciihj^%(aaPBohP6m_YIzfOpW zHktKB(fnU=fE2vsul9)SiU7WERe@#hXqXBVYX(B9kRI7zLKKl8co;t z)VoRc9D&Qw=YhS#P_TRCe1ATfh{?YgWWp<5m~XTx*?~zyT~Y9NvWdJyL*=FlqWsa? zTFf1?<$B;kJTi#jU@M^sl2{+`v_(fVVmdmcZm#$o_{9tpbcvj$--7&De#9S5vAgum zKS~hr@5dmF9B}#wz-k|rob;gabrmys>f$q-qvbJtCd#ZH`GP;wG;WvKWK<=>VX3w* zmmDxt+5_=oGghHNz(-w)QTBihsv|2hzKcP{DL*R&o{{iWblkRYkCHEH)1CTVC|pql zFl4261DeG#E}-_p08c-u8tcbK2C!)+imEO~%EDNXA3kTW%9c)>=?_J@I=ARnvq~aP zI>-A!AA0mbZeiQHv=P*y&gUmJj0Ect<<85wj~9xX%~3Av$mGR1A)W^0;r#nmotII7WWrR#K{DkDX<$s#@^ znF}n;ngil}W;baiixkdKSV0jYFc3pW^?-d~W;mgjmTkaXl5?y&X@}p+g4hFTAqPL+CYnatSk}xnp(e`rL>$Lx~sbJs)@+E2~RTC z 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 WG)A1N8P#gL(x_JUReTD?U;iKVg=5eF literal 0 HcmV?d00001 diff --git a/ldif/base.ldif b/ldif/base.ldif new file mode 100644 index 0000000..105617a --- /dev/null +++ b/ldif/base.ldif @@ -0,0 +1,31 @@ +# Directory Server +dn: #-ld_base_dn-# +objectClass: dcObject +objectClass: organization +dc: #-ld_base_root-# +o: Calculate Directory Server + +# Services +dn: ou=Services,#-ld_base_dn-# +objectClass: top +objectClass: organizationalUnit +ou: Services + +# Admin +dn: #-ld_admin_dn-# +cn: #-ld_admin_login-# +sn: #-ld_admin_login-# +objectClass: person +objectClass: top +description: LDAP Administrator stuff +userPassword: #-ld_admin_hash-# + +# Bind +dn: #-ld_bind_dn-# +cn: #-ld_bind_login-# +sn: #-ld_bind_login-# +objectClass: person +objectClass: top +description: LDAP Proxy User +userPassword: #-ld_bind_hash-# + diff --git a/pym/__init__.py b/pym/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pym/cl_fill_ldap.py b/pym/cl_fill_ldap.py new file mode 100644 index 0000000..d472310 --- /dev/null +++ b/pym/cl_fill_ldap.py @@ -0,0 +1,81 @@ +#-*- coding: utf-8 -*- + +# Copyright 2008-2010 Mir Calculate Ltd. http://www.calculate-linux.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from cl_datavars import glob_attr +from cl_utils import genpassword +from encrypt import encrypt + +class fillVars(glob_attr): + encryptObj = encrypt() + addDn = lambda x,*y: ",".join(y) + genDn = lambda x,*y: "=".join(y) + + def getHash(self, password, encrypt): + """Получить хеш пароля + + password - пароль + encrypt - алгоритм шифрования, например 'ssha' + """ + hashPwd = self.encryptObj.getHashPasswd(password, encrypt.lower()) + if hashPwd: + return hashPwd + else: + print "Error encrypt password, method getHash()" + exit(1) + + def get_ld_base_dn(self): + """базовый DN LDAP""" + return self.genDn("dc", self.Get('ld_base_root')) + + def get_ld_bind_dn(self): + """bind DN LDAP""" + return self.addDn(self.genDn("cn", self.Get('ld_bind_login')), + self.Get('ld_base_dn')) + + def get_ld_bind_hash(self): + """hash пароля для пользователя для чтения""" + return self.getHash(self.Get('ld_bind_pw'), self.Get('ld_encrypt')) + + def get_ld_temp_dn(self): + #DN временного пользователя root (для инициализации базы данных) + return self.addDn(self.genDn("cn", "ldaproot"), self.Get('ld_base_dn')) + + def get_ld_temp_pw(self): + """пароль временного пользователя root""" + return genpassword() + + def get_ld_temp_hash(self): + """hash пароля временного root""" + return self.getHash(self.Get('ld_temp_pw'), self.Get('ld_encrypt')) + + def get_ld_admin_dn(self): + """DN пользователя root""" + return self.addDn(self.genDn("cn", self.Get('ld_admin_login')), + self.Get('ld_base_dn')) + + def get_ld_admin_hash(self): + """hash пароля root""" + return self.getHash(self.Get('ld_admin_pw'), self.Get('ld_encrypt')) + + def get_ld_admin_pw(self): + """пароль root""" + return genpassword() + + def get_ld_services_dn(self): + """DN для всех сервисов""" + return self.addDn(self.genDn("ou", self.Get('ld_services')), + self.Get('ld_base_dn')) diff --git a/pym/cl_ldap_api.py b/pym/cl_ldap_api.py new file mode 100644 index 0000000..1057d3e --- /dev/null +++ b/pym/cl_ldap_api.py @@ -0,0 +1,184 @@ +#-*- coding: utf-8 -*- + +# Copyright 2008-2010 Mir Calculate Ltd. http://www.calculate-linux.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os, sys, re +from cl_print import color_print +from cl_datavars import DataVars +from server.utils import execProg + +from cl_lang import lang +lang().setLanguage(sys.modules[__name__]) + +from cl_abstract import abs_api + +class DataVarsLdap(DataVars): + """Хранение переменных""" + # Имя секции в calculate2.env + envSection = "ldap" + + def importLdap(self, **args): + '''Импорт переменных для calculate-ldap''' + # Импорт переменных + self.importData(self.envSection, ('cl_vars_ldap','cl_fill_ldap')) + +class shareVars: + """share methods template vars""" + # template variables + clVars = False + + def createClVars(self, clVars=False): + """Создает объект Vars""" + if not clVars: + clVars = DataVarsLdap() + # Импортируем переменные + clVars.importLdap() + # Заменяем значения переменных переменными из env файлов + clVars.flIniFile() + # Устанавливаем у объекта атрибут объект переменных + self.clVars = clVars + return True + +class serviceAPI(color_print, shareVars, abs_api): + '''Methods ldap service''' + prioritet = 25 + nameService = "ldap" + nameDaemon = 'slapd' + _templDict = {'name':nameDaemon} + # files + pidFile = '/var/run/openldap/%(name)s.pid' %_templDict + # command + cmdPath = '/etc/init.d/%(name)s' %_templDict + _templDict.update({'cmd':cmdPath}) + cmdStart = '%(cmd)s start' %_templDict + cmdReStart = '%(cmd)s restart' %_templDict + cmdStop = '%(cmd)s stop' %_templDict + cmdShowDaemons = 'rc-update show default' + reShowDaemons = re.compile("(.+)\s+\|\s+.+") + cmdAddRunlevel = 'rc-update add %(name)s default' %_templDict + cmdDelRunlevel = 'rc-update del %(name)s default' %_templDict + + def getServiceName(self): + '''Get name service''' + return self.nameService + + def isSetup(self): + '''Is setup service (True/False)''' + self.createClVars(self.clVars) + return self.clVars.Get('sr_ldap_set') == "on" + + def _getRunlevelDaemons(self): + """Получаем всех демонов в default уровне""" + textLines = execProg(self.cmdShowDaemons) + if textLines is False: + self.printERROR(_("ERROR") + ": " + self.cmdShowDaemons) + return False + else: + daemons = [] + for line in textLines: + res = self.reShowDaemons.search(line) + if res: + daemon = res.groups(0)[0] + daemons.append(daemon) + return daemons + + def isStart(self): + '''Run ldap server (True/False)''' + if os.access(self.pidFile, os.R_OK): + pid = open(self.pidFile).read().strip() + if pid: + procDir = "/proc"+"/"+pid + if os.access(procDir, os.F_OK): + return True + return False + + def start(self): + '''Запускает LDAP сервер''' + if execProg(self.cmdStart) is False: + self.printERROR(_("Can't execute '%s'") %self.cmdStart) + self.printNotOK(_("Starting LDAP") + " ...") + return False + else: + return True + + def restart(self): + '''Перезапускает LDAP сервер''' + if execProg(self.cmdReStart) is False: + self.printERROR(_("Can't execute '%s'") %self.cmdReStart) + self.printNotOK(_("Restarting LDAP")+ " ...") + return False + else: + return True + + def stop(self): + '''Останавливает LDAP сервер''' + if execProg(self.cmdStop) is False: + self.printERROR(_("Can't execute '%s'") %self.cmdStop) + self.printNotOK(_("Stopping LDAP")+ " ...") + return False + else: + return True + + def isRunlevel(self): + '''Находится ли LDAP в автозагрузке''' + daemons = self._getRunlevelDaemons() + if daemons is False: + return False + if self.nameDaemon in daemons: + return True + else: + return False + + def addRunlevel(self): + '''Add daemon to runlevel''' + if not self.isRunlevel(): + if execProg(self.cmdAddRunlevel) is False: + self.printERROR(_("Can't execute '%s'") %self.cmdAddRunlevel) + self.printNotOK(_("service %(name)s added to runlevel")\ + %self._templDict + " ...") + return False + return True + + def delRunlevel(self): + '''Delete daemon from runlevel''' + if self.isRunlevel(): + if execProg(self.cmdDelRunlevel) is False: + self.printERROR(_("Can't execute '%s'") %self.cmdDelRunlevel) + self.printNotOK(_("service %(name)s removed from runlevel")\ + %self._templDict + " ...") + return False + return True + + def getRunPrioritet(self): + '''Get run daemon prioritet''' + return self.prioritet + + def delVarsFromEnv(self): + '''Delete template vars in env files + ''' + self.createClVars(self.clVars) + deleteVariables = ("sr_ldap_set",) + locations = map(lambda x: x[0], self.clVars.Get("cl_env_data")) + for varName in deleteVariables: + for locate in locations: + if not self.clVars.Delete(varName, location=locate, + header=self.clVars.envSection): + fileName = filter(lambda x: x[0] == locate, + self.clVars.Get("cl_env_data"))[0][1] + self.printERROR(_("Can't delete variable '%(name)s' " + "in file %(file)s") %{'name':varName, + 'file':fileName}) + return False + return True diff --git a/pym/cl_ldap_service.py b/pym/cl_ldap_service.py new file mode 100644 index 0000000..3892d0a --- /dev/null +++ b/pym/cl_ldap_service.py @@ -0,0 +1,239 @@ +#-*- coding: utf-8 -*- + +# Copyright 2008-2010 Mir Calculate Ltd. http://www.calculate-linux.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "2.2.0.0" +__app__ = "calculate-ldap" + +import os, sys + +from server.utils import dialogYesNo +from cl_print import color_print + +from cl_ldap_api import serviceAPI, shareVars +from cl_template import template +from server.services import services +from server.ldap import iniLdapParser, ldapFunction, shareldap +from server.utils import genSleep +from cl_utils import removeDir, _error, appendProgramToEnvFile,\ + removeProgramToEnvFile + +from cl_lang import lang +lang().setLanguage(sys.modules[__name__]) + +class ldapService(shareVars, shareldap): + """Методы севисa Ldap""" + # Базовый ldif файл + ldifFileBase = '/usr/lib/calculate-2.2/calculate-ldap/ldif/base.ldif' + apiFile = '/usr/lib/calculate-2.2/calculate-ldap/pym/cl_ldap_api.py' + APIObj = serviceAPI() + servObj = services() + + def applyTemplates(self): + """Apply templates""" + clTempl = template(self.clVars) + dirsFiles = clTempl.applyTemplates() + if clTempl.getError(): + self.printERROR(clTempl.getError().strip()) + return False + else: + return dirsFiles + + def removeLdapDatabase(self): + """Удаляем предыдущую базу данных""" + pathDatabase = "/var/lib/openldap-data" + if os.path.exists(pathDatabase) and os.listdir(pathDatabase): + if os.system("rm /var/lib/openldap-data/* &>/dev/null") !=0: + self.printERROR("Can't remove /var/lib/openldap-data/*") + return False + return True + + if os.path.exists(pathDatabase) and os.listdir(pathDatabase): + removeDir(pathDatabase) + self.printOK(_("Erased LDAP Database") + " ...") + return True + + def connectLdapServer(self): + """Соединяемся с LDAP сервером + + используем DN и пароль временного админстратора + """ + # Если раннее была ошибка то выходим + if self.getError(): + self.printERROR (_("ERROR") + ": " +\ + self.getError().strip()) + return False + tmpDn = self.clVars.Get("ld_temp_dn") + tmpPw = self.clVars.Get("ld_temp_pw") + ldapObj = ldapFunction(tmpDn, tmpPw) + # Генератор задержек + wait = genSleep() + while ldapObj.getError(): + try: + # Задержка + wait.next() + except StopIteration: + break + # Очистка ошибки + _error.error = [] + ldapObj = ldapFunction(tmpDn, tmpPw) + self.ldapObj = ldapObj + self.conLdap = ldapObj.conLdap + if ldapObj.getError(): + # Удаляем одинаковые ошибки + listError = [] + for e in ldapObj.error: + if not e in listError: + listError.append(e) + _error.error = listError + self.printERROR(_("Can not connected to LDAP server")) + return False + return True + + def installProg(self): + '''Install this program''' + apiDict = self.clVars.Get("cl_api") + apiDict.update({__app__:self.apiFile}) + self.clVars.Write("cl_api", force=True) + if not appendProgramToEnvFile(__app__, self.clVars): + self.printERROR(_("Can not save '%s'") %__app__ + " " +\ + _("to %s") %self.clVars.Get("cl_env_path")[0]) + return False + self.printOK(_("Save install variables")) + return True + + def uninstallProg(self): + '''Uninstall this program''' + apiDict = self.clVars.Get("cl_api") + if __app__ in apiDict: + apiDict.pop(__app__) + self.clVars.Write("cl_api", force=True) + if not removeProgramToEnvFile(__app__, self.clVars): + self.printERROR(_("Can not remove '%s' to %s")%(__app__, + self.clVars.Get("cl_env_path")[0])) + return False + self.printOK(_("Delete install variables")) + return True + + def setup(self, force=False): + """Настройка LDAP сервиса (создание дерева)""" + # Принудительная установка + if self.clVars.Get("sr_ldap_set") == "on" and not force: + self.printWARNING (_("WARNING") + ": " +\ + _("LDAP server is configured")+ ".") + return True + if not force: + # предупреждение при выполнении этой программы будут изменены + # конфигурационные файлы и база данных сервиса LDAP а также + # конфигурационные файлы установленных сервисов + self.printWARNING (_("WARNING") + ": " +\ + _("Executing of the program will change") + " " +\ + _("the configuration files and database of LDAP service")+\ + ".") + # если вы готовы продолжить работу программы нажмите Y если нет n + messDialog = \ + _("If you are ready to continue executing the program")+", "+\ + _("input 'yes'") +", "+ _("if not 'no'") + if not dialogYesNo(messDialog): + return True + else: + # делаем backup + # Проверим запущен ли ldap + if not self.APIObj.isStart(): + # Запускаем LDAP сервер + if not self.APIObj.start(): + return False + #if not self.backupServer(): + #return False + if self.APIObj.isRunlevel(): + # Удаляем из автозапуска демона + if not self.APIObj.delRunlevel(): + return False + # Останавливаем все установленные сервисы + if not self.servObj.stopAllServices(): + return False + # Останавливаем LDAP + if self.APIObj.isStart(): + self.APIObj.stop() + # Удаляем из автозагрузки все установленные сервисы + if not self.servObj.delRunlevelAllServices(): + return False + # Удаляем из крона скрипт для чистки удаленых пользователей + # создаем объект репликации + #objRepl = servRepl() + #if not objRepl.cronReplicationOFF(): + #return False + # Удаляем из share файл .replrun + #if not self.servSambaObj.delReplFile(self.clVars): + #return False + # Удаляем переменные + if not self.servObj.delVarsFromAllServices(): + return False + # Получим путь к ldap файлу + ldapParser = iniLdapParser() + ldapFile = ldapParser.nameIniFile + # Удаляем ldap файл + if os.path.exists(ldapFile): + os.remove(ldapFile) + self.clVars.Write("sr_ldap_set", "off",force=True) + self.clVars.Set("sr_ldap_set", "on", force=True) + self.clVars.Set("cl_ldap_setup_action","up", force=True) + # Первый проход + self.clVars.Set("cl_pass_step", "1", True) + if self.applyTemplates() is False: + self.printERROR(_("Can not apply templates") + ":" + " " +\ + _("first pass")) + return False + # Удаляем старую базу данных + if not self.removeLdapDatabase(): + return False + # Запускаем LDAP сервер + if not self.APIObj.start(): + return False + # Соединяемся с LDAP временным пользователем + if not self.connectLdapServer(): + return False + # Получаем текст нужного ldif-a + baseLdif = self.createLdif(self.ldifFileBase) + # Если нет ошибок при соединении применяем ldif + if not self.ldapObj.getError(): + self.ldapObj.ldapAdd(baseLdif) + if self.ldapObj.getError(): + print _("LDAP Error") + ": " + self.ldapObj.getError().strip() + return False + self.printOK(_("Added ldif file") + " ...") + # Второй проход, + # удаляем временного пользователя root из конфигурационного файла + self.clVars.Set("cl_pass_step","2",True) + if self.applyTemplates() is False: + self.printERROR(_("Can not apply profiles") +":"+ _("second pass")) + return False + # Перезапускаем LDAP сервер + if not self.APIObj.restart(): + return False + # Записываем данные администратора сервера + ldapParser.setVar("admin", + {"DN":self.clVars.Get("ld_admin_dn"), + "PASS":self.clVars.Get("ld_admin_pw")}) + # Устанавливаем автозапуск демона + if not self.APIObj.addRunlevel(): + return False + # Записываем переменные для пользователя + #clientVars = ["ur_organization", "ur_signature"] + #if not self.saveVarsClient(clientVars): + #return False + self.clVars.Write("sr_ldap_set","on",force=True) + self.printOK(_("LDAP service configured") + " ...") + return True diff --git a/pym/cl_ldap_setup_cmd.py b/pym/cl_ldap_setup_cmd.py new file mode 100644 index 0000000..14976e1 --- /dev/null +++ b/pym/cl_ldap_setup_cmd.py @@ -0,0 +1,108 @@ +#-*- coding: utf-8 -*- + +# Copyright 2010 Calculate Ltd. http://www.calculate-linux.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from cl_ldap_service import ldapService, __app__, __version__ +from cl_opt import opt +import sys +from cl_share_cmd import share_cmd + +# Перевод сообщений для программы +from cl_lang import lang +lang().setLanguage(sys.modules[__name__]) + +# Использование программы +USAGE = _("%prog [options]") + +# Коментарии к использованию программы +COMMENT_EXAMPLES = _("Create LDAP tree") + +# Пример использования программы +EXAMPLES = _("%prog") + +# Описание программы (что делает программа) +DESCRIPTION = _("Creating LDAP tree") + +# Опции командной строки +CMD_OPTIONS = [{'shortOption':"f", + 'longOption':"force", + 'help':_("forced setup service ldap")}, + {'longOption':"install", + 'help':_("configure the system to install this package")}, + {'longOption':"uninstall", + 'help':_("configure the system to uninstall this package")}] + +class ldap_setup_cmd(share_cmd): + def __init__(self): + # Объект опций командной строки + self.optobj = opt(package=__app__, + version=__version__, + usage=USAGE, + examples=EXAMPLES, + comment_examples=COMMENT_EXAMPLES, + description=DESCRIPTION, + option_list=CMD_OPTIONS +\ + opt.variable_control+opt.color_control, + check_values=self.checkOpts) + # Создаем объект логики + self.logicObj = ldapService() + # Создаем переменные + self.logicObj.createClVars() + # Названия несовместимых опций + self.optionsNamesIncompatible = ["install", "uninstall", "f"] + + def getIncompatibleOptions(self, optObj): + """Получаем несовместимые опции""" + retList = [] + for nameOpt in self.optionsNamesIncompatible: + retList.append(getattr(optObj, nameOpt)) + return retList + + def _getNamesAllSetOptions(self): + """Выдает словарь измененных опций""" + setOptDict = self.optobj.values.__dict__.items() + defaultOptDict = self.optobj.get_default_values().__dict__.items() + return dict(set(setOptDict) - set(defaultOptDict)).keys() + + def getStringIncompatibleOptions(self): + """Форматированная строка несовместимых опций разделенных ','""" + listOpt = list(set(self.optionsNamesIncompatible) &\ + set(self._getNamesAllSetOptions())) + return ", ".join(map(lambda x: len(x) == 1 and "'-%s'"%x or "'--%s'"%x,\ + listOpt)) + + def checkOpts(self, optObj, args): + """Check command line opt and arg""" + if len(filter(lambda x: x, self.getIncompatibleOptions(optObj)))>1: + errMsg = _("incompatible options")+":"+" %s"\ + %self.getStringIncompatibleOptions() + self.optobj.error(errMsg) + return False + if args: + errMsg = _("incorrect argument") + ":" + " %s" %" ".join(args) + self.optobj.error(errMsg) + return False + return optObj, args + + def setup(self, force=False): + """Setup program""" + return self.logicObj.setup(force=force) + + def install(self): + """Инсталяция программы""" + return self.logicObj.installProg() + + def uninstall(self): + """Удаление программы""" + return self.logicObj.uninstallProg() \ No newline at end of file diff --git a/pym/cl_share_cmd.py b/pym/cl_share_cmd.py new file mode 100644 index 0000000..df82189 --- /dev/null +++ b/pym/cl_share_cmd.py @@ -0,0 +1,78 @@ +#-*- coding: utf-8 -*- + +# Copyright 2010 Calculate Ltd. http://www.calculate-linux.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +from cl_print import color_print +from cl_utils import _error + +# Перевод сообщений для программы +from cl_lang import lang +lang().setLanguage(sys.modules[__name__]) + +class share_cmd(color_print, _error): + """Класс общих методов обработки опций командной строки""" + + def printVars(self, optObj): + """Печать переменных""" + if optObj.v: + varsFilter = None + varsNames = [] + format = "default" + # Фильтрование переменных + if optObj.filter: + optCmd = optObj.filter + if ',' in optCmd: + varsNames = optCmd.split(",") + elif '*' in optCmd: + varsFilter = optCmd.replace("*", ".*") + else: + varsNames.append(optCmd) + if optObj.xml: + format = "xml" + self.logicObj.printVars(varsFilter, varsNames, outFormat=format) + + def setVars(self, optObj): + """Установка переменных""" + if optObj.set: + for vals in optObj.set: + for val in vals.split(','): + k,o,v = val.partition('=') + if self.logicObj.clVars.exists(k): + if not self.logicObj.clVars.SetWriteVar(k,v): + return False + else: + self.printERROR(_('variable %s not found')%k) + return False + return True + + def writeVars(self, optObj): + """Запись переменных""" + if optObj.set: + if not self.logicObj.clVars.WriteVars(): + errMsg = self.getError() + if errMsg: + self.printERROR(errMsg.strip()) + self.printERROR(_('Can not write template variables')) + return False + return True + + def setPrintNoColor(self, optObj): + """Установка печати сообщений без цвета""" + if optObj.color and optObj.color=="never": + color_print.colorPrint = lambda *arg : sys.stdout.write(arg[-1]) or\ + sys.stdout.flush() + diff --git a/pym/cl_vars_ldap.py b/pym/cl_vars_ldap.py new file mode 100644 index 0000000..7bcb482 --- /dev/null +++ b/pym/cl_vars_ldap.py @@ -0,0 +1,77 @@ +#-*- coding: utf-8 -*- + +# Copyright 2008-2010 Mir Calculate Ltd. http://www.calculate-linux.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class Data: + #базовый суффикс LDAP + ld_base_dn = {} + + #bind суффикс LDAP + ld_bind_dn = {} + + #пользователь только для чтения + ld_bind_login = {'value':'proxyuser'} + + #hash пароля для пользователя для чтения + ld_bind_hash = {} + + #пароль для пользователя для чтения + ld_bind_pw = {'value':'calculate'} + + #алгоритм шифрования паролей + ld_encrypt = {'value':'ssha'} + + #имя для базового суффикса LDAP + ld_base_root = {'value':'calculate'} + + #временный пользователь root для инициализации базы данных + ld_temp_dn = {} + + #hash пароля временного root + ld_temp_hash = {} + + #пароль временного пользователя root + ld_temp_pw = {} + + #DN пользователя root + ld_admin_dn = {} + + #имя пользователя root для LDAP + ld_admin_login = {'value':'ldapadmin'} + + #hash пароля root + ld_admin_hash = {} + + #пароль root + ld_admin_pw = {} + + #Имя для всех сервисов + ld_services= {'value' : 'Services'} + + #DN всех сервисов + ld_services_dn = {} + + #Настроен или нет сервис LDAP + sr_ldap_set = {'mode':"w",'value':'off'} + + # имя программы + cl_name = {'value':'calculate-ldap'} + + # версия программы + cl_ver = {'value':'2.2.0.0'} + + # действие программа устанавливает сервис + cl_ldap_setup_action = {'value':'down'} diff --git a/scripts/cl-ldap-setup b/scripts/cl-ldap-setup new file mode 100644 index 0000000..5343aa3 --- /dev/null +++ b/scripts/cl-ldap-setup @@ -0,0 +1,65 @@ +#!/usr/bin/python +#-*- coding: utf-8 -*- + +# Copyright 2010 Calculate Ltd. http://www.calculate-linux.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import os +sys.path.insert(0,os.path.abspath('/usr/lib/calculate-2.2/calculate-lib/pym')) +sys.path.insert(0,\ + os.path.abspath('/usr/lib/calculate-2.2/calculate-ldap/pym')) + +from cl_ldap_setup_cmd import ldap_setup_cmd + +from cl_lang import lang +tr = lang() +tr.setGlobalDomain('cl_ldap') +tr.setLanguage(sys.modules[__name__]) + +if __name__ == "__main__": + obj = ldap_setup_cmd() + ret = obj.optobj.parse_args() + if ret is False: + sys.exit(1) + opts, args = ret + # Установка цвета при печати сообщений + obj.setPrintNoColor(opts) + # Установка переменных + if not obj.setVars(opts): + sys.exit(1) + # Печать переменных + obj.printVars(opts) + # Если нет печати переменных выполняем логику программы + if not opts.v and not opts.filter and not opts.xml: + if not filter(lambda x: x[1], obj.optobj.values.__dict__.items()): + # Setup + if not obj.setup(): + sys.exit(1) + elif opts.f: + # Force setup + if not obj.setup(force=True): + sys.exit(1) + elif opts.install: + # Install + if not obj.install(): + sys.exit(1) + elif opts.uninstall: + # Uninstall + if not obj.uninstall(): + sys.exit(1) + # Запись переменных + if not obj.writeVars(opts): + sys.exit(1) + sys.exit(0) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b36081a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[install] +install-scripts=/usr/bin +install-purelib=/usr/lib/calculate-2.2 +install-platlib=/usr/lib/calculate-2.2 +install-data=/usr/lib/calculate-2.2/calculate-ldap \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..7b3ed0f --- /dev/null +++ b/setup.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# setup.py --- Setup script for calculate-client + +#Copyright 2010 Calculate Pack, http://www.calculate-linux.org +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import stat +from distutils.core import setup, Extension + + +__version__ = "2.2.0.0" +__app__ = "calculate-ldap" + + +data_files = [] + +var_data_files = [] + +data_dirs_template = ['templates'] +data_dirs_ldif = ['ldif'] +data_dirs_share = ['i18n'] +share_calculate_dir = "/usr/share/calculate" +template_calculate_dir = os.path.join(share_calculate_dir, "templates") +template_replace_dirname = "ldap" + +def __scanDir(scanDir, prefix, replace_dirname, dirData, flagDir=False): + """Scan directory""" + files = [] + dirs = [] + if flagDir or stat.S_ISDIR(os.stat(scanDir)[stat.ST_MODE]): + for fileOrDir in os.listdir(scanDir): + absPath = os.path.join(scanDir,fileOrDir) + statInfo = os.stat(absPath)[stat.ST_MODE] + if stat.S_ISREG(statInfo): + files.append(absPath) + elif stat.S_ISDIR(statInfo): + dirs.append(absPath) + if replace_dirname: + listDirs = list(scanDir.partition("/"))[1:] + listDirs.insert(0,replace_dirname) + scanDir = "".join(listDirs) + if prefix: + scanDir = os.path.join(prefix,scanDir) + dirData.append((scanDir, files)) + for sDir in dirs: + __scanDir(sDir, prefix, replace_dirname, dirData, True) + return dirData + +def create_data_files(data_dirs, prefix="", replace_dirname=""): + """Create data_files""" + data_files = [] + for data_dir in data_dirs: + data = [] + data_files += __scanDir(data_dir, prefix, replace_dirname, data) + return data_files + +data_files += create_data_files (data_dirs_template, template_calculate_dir, + template_replace_dirname) +data_files += create_data_files (data_dirs_share, share_calculate_dir) +data_files += create_data_files (data_dirs_ldif) + + +setup( + name = __app__, + version = __version__, + description = "The program for configuring LDAP server", + author = "Mir Calculate Ltd.", + author_email = "support@calculate.ru", + url = "http://calculate-linux.org", + license = "http://www.apache.org/licenses/LICENSE-2.0", + package_dir = {'calculate-ldap': "."}, + packages = ['calculate-ldap.pym'], + data_files = data_files, + scripts=["./scripts/cl-ldap-setup"] +) + diff --git a/templates/.calculate_directory b/templates/.calculate_directory new file mode 100644 index 0000000..6e78990 --- /dev/null +++ b/templates/.calculate_directory @@ -0,0 +1 @@ +# Calculate append=skip cl_name==calculate-ldap diff --git a/templates/setup/.calculate_directory b/templates/setup/.calculate_directory new file mode 100644 index 0000000..d57f195 --- /dev/null +++ b/templates/setup/.calculate_directory @@ -0,0 +1 @@ +# Calculate append=skip cl_ldap_setup_action==up diff --git a/templates/setup/openldap/.calculate_directory b/templates/setup/openldap/.calculate_directory new file mode 100644 index 0000000..0e18bc3 --- /dev/null +++ b/templates/setup/openldap/.calculate_directory @@ -0,0 +1,2 @@ +# Calculate belong()!=&&pkg(net-nds/openldap)!= path=/etc name=openldap + diff --git a/templates/setup/openldap/step-1/.calculate_directory b/templates/setup/openldap/step-1/.calculate_directory new file mode 100644 index 0000000..06514e4 --- /dev/null +++ b/templates/setup/openldap/step-1/.calculate_directory @@ -0,0 +1,2 @@ +# Calculate append=skip cl_pass_step==1 + diff --git a/templates/setup/openldap/step-1/slapd.conf b/templates/setup/openldap/step-1/slapd.conf new file mode 100644 index 0000000..97810b2 --- /dev/null +++ b/templates/setup/openldap/step-1/slapd.conf @@ -0,0 +1,60 @@ +# Calculate format=ldap chmod=0640 chown=root:ldap append=replace +include /etc/openldap/schema/core.schema +include /etc/openldap/schema/cosine.schema +include /etc/openldap/schema/nis.schema +include /etc/openldap/schema/inetorgperson.schema +include /etc/openldap/schema/misc.schema +#?pkg(net-nds/openldap)<2.4#schemacheck on#pkg# + +pidfile /var/run/openldap/slapd.pid +argsfile /var/run/openldap/slapd.arg + +# Уровень отладочных сообщений +loglevel 0 +allow bind_v2 +modulepath /usr/lib/openldap/openldap + +# Доступ к аттрибуту userPassword +access to attrs=userPassword + by self write + by dn="#-ld_admin_dn-#" write + by * auth + +# Доступ к администратору сервера LDAP +access to dn.base="#-ld_admin_dn-#" + by dn="#-ld_admin_dn-#" write + by * none + +# Закрываем доступ к веткам +access to dn.regex=".*,#-ld_services_dn-#" + by dn="#-ld_admin_dn-#" write + by * none + +# Доступ ко всем аттрибутам +access to * + by dn="#-ld_admin_dn-#" write + by self write + by * read + +# Доступ по умолчанию только для чтения +#?pkg(net-nds/openldap)<2.4#defaultaccess none#pkg# + +# Тип базы данных +#?pkg(net-nds/openldap)<2.4#database ldbm#pkg# +#?pkg(net-nds/openldap)>2.4#database bdb#pkg# +suffix "#-ld_base_dn-#" +rootdn "#-ld_temp_dn-#" +rootpw #-ld_temp_hash-# +checkpoint 1024 5 +cachesize 10000 +# Размер ответа на запрос +sizelimit unlimited +directory /var/lib/openldap-data + +index objectClass eq +index cn pres,sub,eq +index sn pres,sub,eq +index uid pres,sub,eq +index uidNumber eq +index gidNumber eq +index default sub \ No newline at end of file diff --git a/templates/setup/openldap/step-2/.calculate_directory b/templates/setup/openldap/step-2/.calculate_directory new file mode 100644 index 0000000..2605632 --- /dev/null +++ b/templates/setup/openldap/step-2/.calculate_directory @@ -0,0 +1,2 @@ +# Calculate append=skip cl_pass_step==2 + diff --git a/templates/setup/openldap/step-2/slapd.conf b/templates/setup/openldap/step-2/slapd.conf new file mode 100644 index 0000000..8a1c25d --- /dev/null +++ b/templates/setup/openldap/step-2/slapd.conf @@ -0,0 +1,3 @@ +# Calculate format=ldap chmod=0640 chown=root:ldap append=join +!rootdn del +!rootpw del \ No newline at end of file