From c6c47b90897319429a6348b548fcb168716c20bf Mon Sep 17 00:00:00 2001 From: William Brown Date: Nov 16 2017 11:29:51 +0000 Subject: Ticket 48820 - Proof of concept of orm style mapping of configs and objects Fix Description: This changes the backend type and config to rely on the new _mapped_object types. These abstract and essentially create an orm, allowing us to easily derive and share common operations to our config types. backend.py with the Backends and Backend classes are an excellent example of this https://fedorahosted.org/389/ticket/48820 Author: wibrown Review by: spichugi (Thanks!) --- diff --git a/src/lib389/lib389/_mapped_object.py b/src/lib389/lib389/_mapped_object.py new file mode 100644 index 0000000..2845b84 --- /dev/null +++ b/src/lib389/lib389/_mapped_object.py @@ -0,0 +1,242 @@ +# --- 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 logging + +from lib389._constants import * +from lib389.utils import ensure_bytes, ensure_str + +from lib389._entry import Entry + +# This function filter and term generation provided thanks to +# The University Of Adelaide. + +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, 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. + """ + 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) + + +class DSLdapObject(DSLogging): + 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 = '' + if dn is not None: + self._dn = dn + + self._batch = batch + self._naming_attr = None + self._protected = True + + def __unicode__(self): + val = self._dn + if self._naming_attr: + val = self.get(self._naming_attr) + return ensure_str(val) + + def __str__(self): + return self.__unicode__() + + def set(self, key, value): + 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 self._batch: + pass + else: + return self._instance.modify_s(self._dn, [(ldap.MOD_REPLACE, key, value)]) + + def get(self, key): + """Get an attribute under dn""" + self._log.debug("%s get(%r)" % (self._dn, key)) + if self._instance.state != DIRSRV_STATE_ONLINE: + 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: + return self._instance.getEntry(self._dn).getValues(key) + + def remove(self, key): + """Remove a value defined by key""" + self._log.debug("%s get(%r, %r)" % (self._dn, key, value)) + if self._instance.state != DIRSRV_STATE_ONLINE: + ValueError("Invalid state. Cannot remove properties on instance that is not ONLINE") + else: + # Do a mod_delete on the value. + pass + + 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: + pass + + +# A challenge of this, is how do we manage indexes? They have two naming attribunes.... + +class DSLdapObjects(DSLogging): + def __init__(self, instance, batch=False): + self._childobject = DSLdapObject + self._instance = instance + super(DSLdapObjects, self).__init__(self._instance.verbose) + self._objectclasses = [] + self._create_objectclasses = [] + self._filterattrs = [] + self._list_attrlist = ['dn'] + self._basedn = "" + self._batch = batch + self._scope = ldap.SCOPE_SUBTREE + self._rdn_attribute = None + self._must_attributes = None + + def list(self): + # Filter based on the objectclasses and the basedn + results = self._instance.search_s( + base=self._basedn, + scope=self._scope, + # This will yield and & filter for objectClass with as many terms as needed. + filterstr=_gen_and( + _gen_filter(_term_gen('objectclass'), self._objectclasses) + ), + attrlist=self._list_attrlist, + ) + # def __init__(self, instance, dn=None, batch=False): + insts = map(lambda r: self._childobject(instance=self._instance, dn=r.dn, batch=self._batch), results) + return insts + + def get(self, selector): + # Filter based on the objectclasses and the basedn + # Based on the selector, we should filter on that too. + results = self._instance.search_s( + base=self._basedn, + scope=self._scope, + # This will yield and & filter for objectClass with as many terms as needed. + filterstr=_gen_and( + _gen_filter(_term_gen('objectclass'), self._objectclasses, + extra=_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)) + ) + ) + ), + attrlist=self._list_attrlist, + ) + + 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._childobject(instance=self._instance, dn=results[0].dn, batch=self._batch) + + def _validate(self, rdn, properties): + """ + 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") + + # 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) != list: + raise ldap.UNWILLING_TO_PERFORM("rdn %s from properties is not in a list" % self._rdn_attribute) + if len(trdn) != 1: + raise ldap.UNWILLING_TO_PERFORM("Cannot determine rdn %s from properties. Too many choices" % (self._rdn_attribute)) + rdn = trdn[0] + + if type(rdn) != str: + raise ldap.UNWILLING_TO_PERFORM("rdn %s must be a utf8 string (str)", rdn) + + 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) + + # We may need to map over the data in the properties dict to satisfy python-ldap + # to do str -> bytes + # + # Do we need to fix anything here in the rdn_attribute? + dn = '%s=%s,%s' % (self._rdn_attribute, rdn, self._basedn) + # Do we need to do extra dn validation here? + return (dn, rdn, properties) + + def create(self, rdn=None, properties=None): + assert(len(self._create_objectclasses) > 0) + self._log.debug('Creating %s : %s' % (rdn, properties)) + # Make sure these aren't none. + # Create the dn based on the various properties. + (dn, rdn, valid_props) = self._validate(rdn, properties) + # Check if the entry exists or not? .add_s is going to error anyway ... + self._log.debug('Validated %s : %s' % (dn, properties)) + + e = Entry(dn) + e.update({'objectclass' : self._create_objectclasses}) + e.update(valid_props) + self._instance.add_s(e) + + # Now return the created instance. + return self._childobject(instance=self._instance, dn=dn, batch=self._batch) + diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py index 174bf79..aba4d40 100644 --- a/src/lib389/lib389/backend.py +++ b/src/lib389/lib389/backend.py @@ -10,11 +10,13 @@ import ldap from lib389._constants import * from lib389.properties import * from lib389.utils import normalizeDN -from lib389 import DirSrv, Entry -from lib389 import NoSuchEntryError, InvalidArgumentError +from lib389 import Entry +# Need to fix this .... +from lib389._mapped_object import DSLdapObjects, DSLdapObject +from lib389.exceptions import NoSuchEntryError, InvalidArgumentError -class Backend(object): +class BackendLegacy(object): proxied_methods = 'search_s getEntry'.split() def __init__(self, conn): @@ -24,6 +26,7 @@ class Backend(object): 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): @@ -216,6 +219,8 @@ class Backend(object): 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 @@ -242,13 +247,17 @@ class Backend(object): return bename index += 1 - # suffix is mandatory + # 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 @@ -281,13 +290,14 @@ class Backend(object): % ents[0].dn) # All checks are done, Time to create the backend + import time 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 + BACKEND_PROPNAME_TO_ATTRNAME[BACKEND_SUFFIX]: nsuffix, }) if chained_suffix: @@ -346,12 +356,9 @@ class Backend(object): elif name: filt = "(objectclass=%s)" % BACKEND_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("Backend DN not found: %s" % name) + 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" % @@ -369,3 +376,56 @@ class Backend(object): dn = entries_backend[0].dn replace = [(ldap.MOD_REPLACE, 'nsslapd-require-index', 'on')] self.modify_s(dn, replace) + +class Backend(DSLdapObject): + def __init__(self, instance, dn=None, batch=False): + super(Backend, self).__init__(instance, dn, batch) + self._naming_attr = 'cn' + + def create_sample_entries(self): + self._log.debug('Creating sample entries ....') + +# This only does ldbm backends. Chaining backends are a special case +# of this, so they can be subclassed off. +class Backends(DSLdapObjects): + def __init__(self, instance, batch=False): + super(Backends, self).__init__(instance=instance, batch=False) + self._objectclasses = [BACKEND_OBJECTCLASS_VALUE] + self._create_objectclasses = self._objectclasses + ['top', 'extensibleObject' ] + self._filterattrs = ['cn', 'nsslapd-suffix', 'nsslapd-directory'] + self._basedn = DN_LDBM + self._childobject = Backend + self._rdn_attribute = 'cn' + self._must_attributes = ['nsslapd-suffix', 'cn'] + + def _validate(self, rdn, properties): + # 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(): + nprops[BACKEND_PROPNAME_TO_ATTRNAME[key]] = [value,] + + (dn, rdn, valid_props) = super(Backends, self)._validate(rdn, nprops) + + return (dn, rdn, nprops) + + def create(self, rdn=None, properties=None): + # properties for a backend might contain a key called BACKEND_SAMPLE_ENTRIES + # We need to pop this value out, and pass it to our new instance. + sample_entries = properties.pop(BACKEND_SAMPLE_ENTRIES, False) + be_inst = super(Backends, self).create(rdn, properties) + if sample_entries is True: + be_inst.create_sample_entries() + return be_inst + + diff --git a/src/lib389/lib389/brooker.py b/src/lib389/lib389/brooker.py deleted file mode 100644 index 11f018f..0000000 --- a/src/lib389/lib389/brooker.py +++ /dev/null @@ -1,164 +0,0 @@ -# --- 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 - - -class Config(object): - """ - Manage "cn=config" tree, including: - - enable SSL - - set access and error logging - - get and set "cn=config" attributes - """ - def __init__(self, conn): - """@param conn - a DirSrv instance """ - self.conn = conn - self.log = conn.log - - def set(self, key, value): - """Set a parameter under cn=config - @param key - the attribute name - @param value - attribute value as string - - eg. set('passwordExp', 'on') - """ - self.log.debug("set(%r, %r)" % (key, value)) - return self.conn.modify_s(DN_CONFIG, [(ldap.MOD_REPLACE, key, value)]) - - def get(self, key): - """Get an attribute under cn=config""" - return self.conn.getEntry(DN_CONFIG).__getattr__(key) - - 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 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("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.conn.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.conn.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.conn.modify_s(DN_CONFIG, mod) - - fields = 'nsslapd-security nsslapd-ssl-check-hostname'.split() - return self.conn.getEntry(DN_CONFIG, attrlist=fields) diff --git a/src/lib389/lib389/config.py b/src/lib389/lib389/config.py new file mode 100644 index 0000000..d4c00a7 --- /dev/null +++ b/src/lib389/lib389/config.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 --- + +"""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 + +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.conn = conn + #self.log = conn.log + + 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) + + #### 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("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.conn.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.conn.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.conn.modify_s(DN_CONFIG, mod) + + fields = 'nsslapd-security nsslapd-ssl-check-hostname'.split() + return self.conn.getEntry(DN_CONFIG, attrlist=fields)