diff options
Diffstat (limited to 'ipaserver/install/ldapupdate.py')
-rw-r--r-- | ipaserver/install/ldapupdate.py | 688 |
1 files changed, 357 insertions, 331 deletions
diff --git a/ipaserver/install/ldapupdate.py b/ipaserver/install/ldapupdate.py index 949b86ad9..a51edff99 100644 --- a/ipaserver/install/ldapupdate.py +++ b/ipaserver/install/ldapupdate.py @@ -33,9 +33,8 @@ import uuid from ipalib import util from ipalib import errors from ipalib import api -from ipalib.dn import DN +from ipapython.dn import DN import ldap -from ldap.dn import escape_dn_chars from ipapython.ipa_log_manager import * import krbV import platform @@ -52,22 +51,62 @@ from ipaserver.install.plugins import FIRST, MIDDLE, LAST class BadSyntax(installutils.ScriptError): def __init__(self, value): self.value = value - self.msg = "There is a syntax error in this update file: \n %s" % value + self.msg = "LDAPUpdate: syntax error: \n %s" % value self.rval = 1 def __str__(self): return repr(self.value) class LDAPUpdate: + action_keywords = ["default", "add", "remove", "only", "deleteentry", "replace", "addifnew", "addifexist"] + def __init__(self, dm_password, sub_dict={}, live_run=True, online=True, ldapi=False, plugins=False): - """dm_password = Directory Manager password - sub_dict = substitution dictionary - live_run = Apply the changes or just test - online = do an online LDAP update or use an experimental LDIF updater - ldapi = bind using ldapi. This assumes autobind is enabled. - plugins = execute the pre/post update plugins - """ + ''' + :parameters: + dm_password + Directory Manager password + sub_dict + substitution dictionary + live_run + Apply the changes or just test + online + Do an online LDAP update or use an experimental LDIF updater + ldapi + Bind using ldapi. This assumes autobind is enabled. + plugins + execute the pre/post update plugins + + Data Structure Example: + ----------------------- + + dn_by_rdn_count = { + 3: 'cn=config,dc=example,dc=com': + 4: 'cn=bob,ou=people,dc=example,dc=com', + } + + all_updates = { + 'dn': 'cn=config,dc=example,dc=com': + { + 'dn': 'cn=config,dc=example,dc=com', + 'default': ['attr1':default1'], + 'updates': ['action:attr1:value1', + 'action:attr2:value2] + }, + 'dn': 'cn=bob,ou=people,dc=example,dc=com': + { + 'dn': 'cn=bob,ou=people,dc=example,dc=com', + 'default': ['attr3':default3'], + 'updates': ['action:attr3:value3', + 'action:attr4:value4], + } + } + + The default and update lists are "dispositions" + + + ''' + log_mgr.get_logger(self, True) self.sub_dict = sub_dict self.live_run = live_run self.dm_password = dm_password @@ -77,6 +116,8 @@ class LDAPUpdate: self.ldapi = ldapi self.plugins = plugins self.pw_name = pwd.getpwuid(os.geteuid()).pw_name + self.realm = None + suffix = None if sub_dict.get("REALM"): self.realm = sub_dict["REALM"] @@ -89,8 +130,10 @@ class LDAPUpdate: self.realm = None suffix = None + if suffix is not None: + assert isinstance(suffix, DN) domain = ipautil.get_domain_name() - libarch = self.__identify_arch() + libarch = self._identify_arch() fqdn = installutils.get_fqdn() if fqdn is None: @@ -110,7 +153,7 @@ class LDAPUpdate: if not self.sub_dict.get("SUFFIX") and suffix is not None: self.sub_dict["SUFFIX"] = suffix if not self.sub_dict.get("ESCAPED_SUFFIX"): - self.sub_dict["ESCAPED_SUFFIX"] = escape_dn_chars(suffix) + self.sub_dict["ESCAPED_SUFFIX"] = str(suffix) if not self.sub_dict.get("LIBARCH"): self.sub_dict["LIBARCH"] = libarch if not self.sub_dict.get("TIME"): @@ -123,7 +166,7 @@ class LDAPUpdate: try: conn = ipaldap.IPAdmin(fqdn, ldapi=self.ldapi, realm=self.realm) if self.dm_password: - conn.do_simple_bind(binddn="cn=directory manager", bindpw=self.dm_password) + conn.do_simple_bind(binddn=DN(('cn', 'directory manager')), bindpw=self.dm_password) elif os.getegid() == 0: try: # autobind @@ -145,13 +188,13 @@ class LDAPUpdate: # The following 2 functions were taken from the Python # documentation at http://docs.python.org/library/csv.html - def __utf_8_encoder(self, unicode_csv_data): + def _utf_8_encoder(self, unicode_csv_data): for line in unicode_csv_data: yield line.encode('utf-8') - def __unicode_csv_reader(self, unicode_csv_data, quote_char="'", dialect=csv.excel, **kwargs): + def _unicode_csv_reader(self, unicode_csv_data, quote_char="'", dialect=csv.excel, **kwargs): # csv.py doesn't do Unicode; encode temporarily as UTF-8: - csv_reader = csv.reader(self.__utf_8_encoder(unicode_csv_data), + csv_reader = csv.reader(self._utf_8_encoder(unicode_csv_data), dialect=dialect, delimiter=',', quotechar=quote_char, skipinitialspace=True, @@ -160,7 +203,7 @@ class LDAPUpdate: # decode UTF-8 back to Unicode, cell by cell: yield [unicode(cell, 'utf-8') for cell in row] - def __identify_arch(self): + def _identify_arch(self): """On multi-arch systems some libraries may be in /lib64, /usr/lib64, etc. Determine if a suffix is needed based on the current architecture. @@ -178,7 +221,7 @@ class LDAPUpdate: except KeyError, e: raise BadSyntax("Unknown template keyword %s" % e) - def __parse_values(self, line): + def _parse_values(self, line): """Parse a comma-separated string into separate values and convert them into a list. This should handle quoted-strings with embedded commas """ @@ -186,7 +229,7 @@ class LDAPUpdate: quote_char = "'" else: quote_char = '"' - reader = self.__unicode_csv_reader([line], quote_char) + reader = self._unicode_csv_reader([line], quote_char) value = [] for row in reader: value = value + row @@ -201,7 +244,7 @@ class LDAPUpdate: if fd != sys.stdin: fd.close() return text - def __entry_to_entity(self, ent): + def _entry_to_entity(self, ent): """Tne Entry class is a bare LDAP entry. The Entity class has a lot more helper functions that we need, so convert to dict and then to Entity. """ @@ -215,113 +258,158 @@ class LDAPUpdate: entry[key] = value[0] return entity.Entity(entry) - def __combine_updates(self, dn_list, all_updates, update): - """Combine a new update with the list of total updates - - Updates are stored in 2 lists: - dn_list: contains a unique list of DNs in the updates - all_updates: the actual updates that need to be applied - - We want to apply the updates from the shortest to the longest - path so if new child and parent entries are in different updates - we can be sure the parent gets written first. This also lets - us apply any schema first since it is in the very short cn=schema. - """ + def _combine_updates(self, all_updates, update): + 'Combine a new update with the list of total updates' dn = update.get('dn') - dns = ldap.explode_dn(dn.lower()) - l = len(dns) - if dn_list.get(l): - if dn not in dn_list[l]: - dn_list[l].append(dn) - else: - dn_list[l] = [dn] + assert isinstance(dn, DN) + if not all_updates.get(dn): all_updates[dn] = update return all_updates - e = all_updates[dn] + existing_update = all_updates[dn] if 'default' in update: - if 'default' in e: - e['default'] = e['default'] + update['default'] - else: - e['default'] = update['default'] + disposition_list = existing_update.setdefault('default', []) + disposition_list.extend(update['default']) elif 'updates' in update: - if 'updates' in e: - e['updates'] = e['updates'] + update['updates'] - else: - e['updates'] = update['updates'] + disposition_list = existing_update.setdefault('updates', []) + disposition_list.extend(update['updates']) else: - root_logger.debug("Unknown key in updates %s" % update.keys()) - - all_updates[dn] = e + self.debug("Unknown key in updates %s" % update.keys()) + + def merge_updates(self, all_updates, updates): + ''' + Add the new_update dict to the all_updates dict. If an entry + in the new_update already has an entry in all_updates merge + the two entries sensibly assuming the new entries take + precedence. Otherwise just add the new entry. + ''' + + for new_update in updates: + for new_dn, new_entry in new_update.iteritems(): + existing_entry = all_updates.get(new_dn) + if existing_entry: + # If the existing entry is marked for deletion but the + # new entry is not also a delete then clear the delete + # flag otherwise the newer update will be lost. + if existing_entry.has_key('deleteentry') and not new_entry.has_key('deleteentry'): + self.warning("ldapupdate: entry '%s' previously marked for deletion but" + + " this subsequent update reestablishes it: %s", new_dn, new_entry) + del existing_entry['deleteentry'] + existing_entry.update(new_entry) + else: + all_updates[new_dn] = new_entry - return all_updates - def parse_update_file(self, data, all_updates, dn_list): + def parse_update_file(self, data_source_name, source_data, all_updates): """Parse the update file into a dictonary of lists and apply the update for each DN in the file.""" - valid_keywords = ["default", "add", "remove", "only", "deleteentry", "replace", "addifnew", "addifexist"] update = {} - d = "" - index = "" + logical_line = "" + action = "" dn = None lcount = 0 - for line in data: - # Strip out \n and extra white space - lcount = lcount + 1 - # skip comments and empty lines - line = line.rstrip() - if line.startswith('#') or line == '': continue + def emit_item(logical_line): + ''' + Given a logical line containing an item to process perform the following: - if line.lower().startswith('dn:'): - if dn is not None: - all_updates = self.__combine_updates(dn_list, all_updates, update) + * Strip leading & trailing whitespace + * Substitute any variables + * Get the action, attribute, and value + * Each update has one list per disposition, append to specified disposition list + ''' - update = {} - dn = line[3:].strip() - update['dn'] = self._template_str(dn) - else: - if dn is None: - raise BadSyntax, "dn is not defined in the update" - - line = self._template_str(line) - if line.startswith(' '): - v = d[len(d) - 1] - v = v + line[1:] - d[len(d) - 1] = v - update[index] = d - continue - line = line.strip() - values = line.split(':', 2) - if len(values) != 3: - raise BadSyntax, "Bad formatting on line %d: %s" % (lcount,line) + logical_line = logical_line.strip() + if logical_line == '': + return + + # Perform variable substitution on constructued line + logical_line = self._template_str(logical_line) - index = values[0].strip().lower() + items = logical_line.split(':', 2) - if index not in valid_keywords: - raise BadSyntax, "Unknown keyword %s" % index + if len(items) == 0: + raise BadSyntax, "Bad formatting on line %s:%d: %s" % (data_source_name, lcount, logical_line) - attr = values[1].strip() - value = values[2].strip() + action = items[0].strip().lower() - new_value = "" - if index == "default": + if action not in self.action_keywords: + raise BadSyntax, "Unknown update action '%s', data source=%s" % (action, data_source_name) + + if action == 'deleteentry': + new_value = None + disposition = "deleteentry" + else: + if len(items) != 3: + raise BadSyntax, "Bad formatting on line %s:%d: %s" % (data_source_name, lcount, logical_line) + + attr = items[1].strip() + value = items[2].strip() + + if action == "default": new_value = attr + ":" + value + disposition = "default" else: - new_value = index + ":" + attr + ":" + value - index = "updates" + new_value = action + ":" + attr + ":" + value + disposition = "updates" + + disposition_list = update.setdefault(disposition, []) + disposition_list.append(new_value) + + def emit_update(update): + ''' + When processing a dn is completed emit the update by merging it into + the set of all updates. + ''' + + self._combine_updates(all_updates, update) + + # Iterate over source input lines + for source_line in source_data: + lcount += 1 + + # strip trailing whitespace and newline + source_line = source_line.rstrip() - d = update.get(index, []) + # skip comments and empty lines + if source_line.startswith('#') or source_line == '': + continue - d.append(new_value) + if source_line.lower().startswith('dn:'): + # Starting new dn + if dn is not None: + # Emit previous dn + emit_item(logical_line) + logical_line = '' + emit_update(update) + update = {} + + dn = source_line[3:].strip() + dn = DN(self._template_str(dn)) + update['dn'] = dn + else: + # Process items belonging to dn + if dn is None: + raise BadSyntax, "dn is not defined in the update, data source=%s" % (data_source_name) - update[index] = d + # If continuation line, append to existing logical line & continue, + # otherwise flush the previous item. + if source_line.startswith(' '): + logical_line += source_line[1:] + continue + else: + emit_item(logical_line) + logical_line = '' + logical_line = source_line if dn is not None: - all_updates = self.__combine_updates(dn_list, all_updates, update) + emit_item(logical_line) + logical_line = '' + emit_update(update) + update = {} - return (all_updates, dn_list) + return all_updates def create_index_task(self, attribute): """Create a task to update an index for an attribute""" @@ -337,15 +425,15 @@ class LDAPUpdate: cn = "indextask_%s_%s_%s" % (attribute, cn_uuid.time, cn_uuid.clock_seq) dn = DN(('cn', cn), ('cn', 'index'), ('cn', 'tasks'), ('cn', 'config')) - e = ipaldap.Entry(str(dn)) + e = ipaldap.Entry(dn) e.setValues('objectClass', ['top', 'extensibleObject']) e.setValue('cn', cn) e.setValue('nsInstance', 'userRoot') e.setValues('nsIndexAttribute', attribute) - root_logger.info("Creating task to index attribute: %s", attribute) - root_logger.debug("Task id: %s", dn) + self.info("Creating task to index attribute: %s", attribute) + self.debug("Task id: %s", dn) if self.live_run: self.conn.addEntry(e) @@ -356,6 +444,8 @@ class LDAPUpdate: """Give a task DN monitor it and wait until it has completed (or failed) """ + assert isinstance(dn, DN) + if not self.live_run: # If not doing this live there is nothing to monitor return @@ -370,10 +460,10 @@ class LDAPUpdate: try: entry = self.conn.getEntry(dn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist) except errors.NotFound, e: - root_logger.error("Task not found: %s", dn) + self.error("Task not found: %s", dn) return except errors.DatabaseError, e: - root_logger.error("Task lookup failure %s", e) + self.error("Task lookup failure %s", e) return status = entry.getValue('nstaskstatus') @@ -383,200 +473,158 @@ class LDAPUpdate: continue if status.lower().find("finished") > -1: - root_logger.info("Indexing finished") + self.info("Indexing finished") break - root_logger.debug("Indexing in progress") + self.debug("Indexing in progress") time.sleep(1) return - def __create_default_entry(self, dn, default): + def _create_default_entry(self, dn, default): """Create the default entry from the values provided. The return type is entity.Entity """ + assert isinstance(dn, DN) entry = ipaldap.Entry(dn) if not default: # This means that the entire entry needs to be created with add - return self.__entry_to_entity(entry) + return self._entry_to_entity(entry) - for line in default: + for item in default: # We already do syntax-parsing so this is safe - (k, v) = line.split(':',1) - e = entry.getValues(k) + (attr, value) = item.split(':',1) + e = entry.getValues(attr) if e: # multi-valued attribute e = list(e) - e.append(v) + e.append(value) else: - e = v - entry.setValues(k, e) + e = value + entry.setValues(attr, e) - return self.__entry_to_entity(entry) + return self._entry_to_entity(entry) - def __get_entry(self, dn): + def _get_entry(self, dn): """Retrieve an object from LDAP. The return type is ipaldap.Entry """ + assert isinstance(dn, DN) searchfilter="objectclass=*" sattrs = ["*", "aci", "attributeTypes", "objectClasses"] scope = ldap.SCOPE_BASE return self.conn.getList(dn, scope, searchfilter, sattrs) - def __update_managed_entries(self): - """Update and move legacy Managed Entry Plugins.""" - - suffix = ipautil.realm_to_suffix(self.realm) - searchfilter = '(objectclass=*)' - definitions_managed_entries = [] - old_template_container = 'cn=etc,%s' % suffix - old_definition_container = 'cn=Managed Entries,cn=plugins,cn=config' - new = 'cn=Managed Entries,cn=etc,%s' % suffix - sub = ['cn=Definitions,', 'cn=Templates,'] - new_managed_entries = [] - old_templates = [] - template = None - try: - definitions_managed_entries = self.conn.getList(old_definition_container, ldap.SCOPE_ONELEVEL, searchfilter,[]) - except errors.NotFound, e: - return new_managed_entries - for entry in definitions_managed_entries: - new_definition = {} - definition_managed_entry_updates = {} - definitions_managed_entries - old_definition = {'dn': entry.dn, 'deleteentry': ['dn: %s' % entry.dn]} - old_template = entry.getValue('managedtemplate') - entry.setValues('managedtemplate', entry.getValue('managedtemplate').replace(old_template_container, sub[1] + new)) - new_definition['dn'] = entry.dn.replace(old_definition_container, sub[0] + new) - new_definition['default'] = str(entry).strip().replace(': ', ':').split('\n')[1:] - definition_managed_entry_updates[new_definition['dn']] = new_definition - definition_managed_entry_updates[old_definition['dn']] = old_definition - old_templates.append(old_template) - new_managed_entries.append(definition_managed_entry_updates) - for old_template in old_templates: - try: - template = self.conn.getEntry(old_template, ldap.SCOPE_BASE, searchfilter,[]) - new_template = {} - template_managed_entry_updates = {} - old_template = {'dn': template.dn, 'deleteentry': ['dn: %s' % template.dn]} - new_template['dn'] = template.dn.replace(old_template_container, sub[1] + new) - new_template['default'] = str(template).strip().replace(': ', ':').split('\n')[1:] - template_managed_entry_updates[new_template['dn']] = new_template - template_managed_entry_updates[old_template['dn']] = old_template - new_managed_entries.append(template_managed_entry_updates) - except errors.NotFound, e: - pass - if len(new_managed_entries) > 0: - new_managed_entries.sort(reverse=True) - - return new_managed_entries - - def __apply_updates(self, updates, entry): - """updates is a list of changes to apply - entry is the thing to apply them to + def _apply_update_disposition(self, updates, entry): + """ + updates is a list of changes to apply + entry is the thing to apply them to - Returns the modified entry + Returns the modified entry """ if not updates: return entry only = {} - for u in updates: + for update in updates: # We already do syntax-parsing so this is safe - (utype, k, values) = u.split(':',2) - values = self.__parse_values(values) - - e = entry.getValues(k) - if not isinstance(e, list): - if e is None: - e = [] + (action, attr, update_values) = update.split(':',2) + update_values = self._parse_values(update_values) + + # If the attribute is known to be a DN convert it to a DN object. + # This has to be done after _parse_values() due to quoting and comma separated lists. + if self.conn.has_dn_syntax(attr): + update_values = [DN(x) for x in update_values] + + entry_values = entry.getValues(attr) + if not isinstance(entry_values, list): + if entry_values is None: + entry_values = [] else: - e = [e] - for v in values: - if utype == 'remove': - root_logger.debug("remove: '%s' from %s, current value %s", v, k, e) + entry_values = [entry_values] + for update_value in update_values: + if action == 'remove': + self.debug("remove: '%s' from %s, current value %s", update_value, attr, entry_values) try: - e.remove(v) + entry_values.remove(update_value) except ValueError: - root_logger.warning("remove: '%s' not in %s", v, k) + self.warning("remove: '%s' not in %s", update_value, attr) pass - entry.setValues(k, e) - root_logger.debug('remove: updated value %s', e) - elif utype == 'add': - root_logger.debug("add: '%s' to %s, current value %s", v, k, e) + entry.setValues(attr, entry_values) + self.debug('remove: updated value %s', entry_values) + elif action == 'add': + self.debug("add: '%s' to %s, current value %s", update_value, attr, entry_values) # Remove it, ignoring errors so we can blindly add it later try: - e.remove(v) + entry_values.remove(update_value) except ValueError: pass - e.append(v) - root_logger.debug('add: updated value %s', e) - entry.setValues(k, e) - elif utype == 'addifnew': - root_logger.debug("addifnew: '%s' to %s, current value %s", v, k, e) + entry_values.append(update_value) + self.debug('add: updated value %s', entry_values) + entry.setValues(attr, entry_values) + elif action == 'addifnew': + self.debug("addifnew: '%s' to %s, current value %s", update_value, attr, entry_values) # Only add the attribute if it doesn't exist. Only works # with single-value attributes. - if len(e) == 0: - e.append(v) - root_logger.debug('addifnew: set %s to %s', k, e) - entry.setValues(k, e) - elif utype == 'addifexist': - root_logger.debug("addifexist: '%s' to %s, current value %s", v, k, e) + if len(entry_values) == 0: + entry_values.append(update_value) + self.debug('addifnew: set %s to %s', attr, entry_values) + entry.setValues(attr, entry_values) + elif action == 'addifexist': + self.debug("addifexist: '%s' to %s, current value %s", update_value, attr, entry_values) # Only add the attribute if the entry doesn't exist. We # determine this based on whether it has an objectclass if entry.getValues('objectclass'): - e.append(v) - root_logger.debug('addifexist: set %s to %s', k, e) - entry.setValues(k, e) - elif utype == 'only': - root_logger.debug("only: set %s to '%s', current value %s", k, v, e) - if only.get(k): - e.append(v) + entry_values.append(update_value) + self.debug('addifexist: set %s to %s', attr, entry_values) + entry.setValues(attr, entry_values) + elif action == 'only': + self.debug("only: set %s to '%s', current value %s", attr, update_value, entry_values) + if only.get(attr): + entry_values.append(update_value) else: - e = [v] - only[k] = True - entry.setValues(k, e) - root_logger.debug('only: updated value %s', e) - elif utype == 'deleteentry': + entry_values = [update_value] + only[attr] = True + entry.setValues(attr, entry_values) + self.debug('only: updated value %s', entry_values) + elif action == 'deleteentry': # skip this update type, it occurs in __delete_entries() return None - elif utype == 'replace': - # v has the format "old::new" + elif action == 'replace': + # value has the format "old::new" try: - (old, new) = v.split('::', 1) + (old, new) = update_value.split('::', 1) except ValueError: - raise BadSyntax, "bad syntax in replace, needs to be in the format old::new in %s" % v + raise BadSyntax, "bad syntax in replace, needs to be in the format old::new in %s" % update_value try: - e.remove(old) - e.append(new) - root_logger.debug('replace: updated value %s', e) - entry.setValues(k, e) + entry_values.remove(old) + entry_values.append(new) + self.debug('replace: updated value %s', entry_values) + entry.setValues(attr, entry_values) except ValueError: - root_logger.debug('replace: %s not found, skipping', old) - - self.print_entity(entry) + self.debug('replace: %s not found, skipping', old) return entry def print_entity(self, e, message=None): """The entity object currently lacks a str() method""" - root_logger.debug("---------------------------------------------") + self.debug("---------------------------------------------") if message: - root_logger.debug("%s", message) - root_logger.debug("dn: " + e.dn) + self.debug("%s", message) + self.debug("dn: %s", e.dn) attr = e.attrList() for a in attr: value = e.getValues(a) - if isinstance(value,str): - root_logger.debug(a + ": " + value) - else: - root_logger.debug(a + ": ") + if isinstance(value, (list, tuple)): + self.debug('%s:', a) for l in value: - root_logger.debug("\t" + l) + self.debug("\t%s", l) + else: + self.debug('%s: %s', a, value) def is_schema_updated(self, s): """Compare the schema in 's' with the current schema in the DS to @@ -598,48 +646,59 @@ class LDAPUpdate: s = s.ldap_entry() # Get a fresh copy and convert into a SubSchema - n = self.__get_entry("cn=schema")[0] - n = dict(n.data) - n = ldap.schema.SubSchema(n) + n = self._get_entry(DN(('cn', 'schema')))[0] + + # Convert IPA data types back to strings + d = dict() + for k,v in n.data.items(): + d[k] = [str(x) for x in v] + + # Convert to subschema dict + n = ldap.schema.SubSchema(d) n = n.ldap_entry() + # Are they equal? if s == n: return False else: return True - def __update_record(self, update): + def _update_record(self, update): found = False - new_entry = self.__create_default_entry(update.get('dn'), - update.get('default')) + # If the entry is going to be deleted no point in processing it. + if update.has_key('deleteentry'): + return + + new_entry = self._create_default_entry(update.get('dn'), + update.get('default')) try: - e = self.__get_entry(new_entry.dn) + e = self._get_entry(new_entry.dn) if len(e) > 1: # we should only ever get back one entry raise BadSyntax, "More than 1 entry returned on a dn search!? %s" % new_entry.dn - entry = self.__entry_to_entity(e[0]) + entry = self._entry_to_entity(e[0]) found = True - root_logger.info("Updating existing entry: %s", entry.dn) + self.info("Updating existing entry: %s", entry.dn) except errors.NotFound: # Doesn't exist, start with the default entry entry = new_entry - root_logger.info("New entry: %s", entry.dn) + self.info("New entry: %s", entry.dn) except errors.DatabaseError: # Doesn't exist, start with the default entry entry = new_entry - root_logger.info("New entry, using default value: %s", entry.dn) + self.info("New entry, using default value: %s", entry.dn) - self.print_entity(entry) + self.print_entity(entry, "Initial value") # Bring this entry up to date - entry = self.__apply_updates(update.get('updates'), entry) + entry = self._apply_update_disposition(update.get('updates'), entry) if entry is None: # It might be None if it is just deleting an entry return - self.print_entity(entry, "Final value") + self.print_entity(entry, "Final value after applying updates") if not found: # New entries get their orig_data set to the entry itself. We want to @@ -657,47 +716,47 @@ class LDAPUpdate: except errors.NotFound: # parent entry of the added entry does not exist # this may not be an error (e.g. entries in NIS container) - root_logger.info("Parent DN of %s may not exist, cannot create the entry", + self.info("Parent DN of %s may not exist, cannot create the entry", entry.dn) return self.modified = True except Exception, e: - root_logger.error("Add failure %s", e) + self.error("Add failure %s", e) else: # Update LDAP try: updated = False changes = self.conn.generateModList(entry.origDataDict(), entry.toDict()) - if (entry.dn == "cn=schema"): + if (entry.dn == DN(('cn', 'schema'))): updated = self.is_schema_updated(entry.toDict()) else: if len(changes) >= 1: updated = True - root_logger.debug("%s" % changes) - root_logger.debug("Live %d, updated %d" % (self.live_run, updated)) + self.debug("%s" % changes) + self.debug("Live %d, updated %d" % (self.live_run, updated)) if self.live_run and updated: self.conn.updateEntry(entry.dn, entry.origDataDict(), entry.toDict()) - root_logger.info("Done") + self.info("Done") except errors.EmptyModlist: - root_logger.info("Entry already up-to-date") + self.info("Entry already up-to-date") updated = False except errors.DatabaseError, e: - root_logger.error("Update failed: %s", e) + self.error("Update failed: %s", e) updated = False except errors.ACIError, e: - root_logger.error("Update failed: %s", e) + self.error("Update failed: %s", e) updated = False - if ("cn=index" in entry.dn and - "cn=userRoot" in entry.dn): - taskid = self.create_index_task(entry.cn) + if (DN(('cn', 'index')) in entry.dn and + DN(('cn', 'userRoot')) in entry.dn): + taskid = self.create_index_task(entry.getValue('cn')) self.monitor_index_task(taskid) if updated: self.modified = True return - def __delete_record(self, updates): + def _delete_record(self, updates): """ Run through all the updates again looking for any that should be deleted. @@ -706,38 +765,21 @@ class LDAPUpdate: considered first so we don't end up trying to delete a parent and child in the wrong order. """ - dn = updates['dn'] - deletes = updates.get('deleteentry', []) - for d in deletes: - try: - root_logger.info("Deleting entry %s", dn) - if self.live_run: - self.conn.deleteEntry(dn) - self.modified = True - except errors.NotFound, e: - root_logger.info("%s did not exist:%s", dn, e) - self.modified = True - except errors.DatabaseError, e: - root_logger.error("Delete failed: %s", e) - - updates = updates.get('updates', []) - for u in updates: - # We already do syntax-parsing so this is safe - (utype, k, values) = u.split(':',2) - if utype == 'deleteentry': - try: - root_logger.info("Deleting entry %s", dn) - if self.live_run: - self.conn.deleteEntry(dn) - self.modified = True - except errors.NotFound, e: - root_logger.info("%s did not exist:%s", dn, e) - self.modified = True - except errors.DatabaseError, e: - root_logger.error("Delete failed: %s", e) + if not updates.has_key('deleteentry'): + return - return + dn = updates['dn'] + try: + self.info("Deleting entry %s", dn) + if self.live_run: + self.conn.deleteEntry(dn) + self.modified = True + except errors.NotFound, e: + self.info("%s did not exist:%s", dn, e) + self.modified = True + except errors.DatabaseError, e: + self.error("Delete failed: %s", e) def get_all_files(self, root, recursive=False): """Get all update files""" @@ -761,7 +803,7 @@ class LDAPUpdate: realm=self.realm) try: if self.dm_password: - self.conn.do_simple_bind(binddn="cn=directory manager", bindpw=self.dm_password) + self.conn.do_simple_bind(binddn=DN(('cn', 'directory manager')), bindpw=self.dm_password) elif os.getegid() == 0: try: # autobind @@ -776,19 +818,28 @@ class LDAPUpdate: else: raise RuntimeError("Offline updates are not supported.") - def __run_updates(self, dn_list, all_updates): + def _run_updates(self, all_updates): # For adds and updates we want to apply updates from shortest # to greatest length of the DN. For deletes we want the reverse. - sortedkeys = dn_list.keys() + + dn_by_rdn_count = {} + for dn in all_updates.keys(): + assert isinstance(dn, DN) + rdn_count = len(dn) + rdn_count_list = dn_by_rdn_count.setdefault(rdn_count, []) + if dn not in rdn_count_list: + rdn_count_list.append(dn) + + sortedkeys = dn_by_rdn_count.keys() sortedkeys.sort() - for k in sortedkeys: - for dn in dn_list[k]: - self.__update_record(all_updates[dn]) + for rdn_count in sortedkeys: + for dn in dn_by_rdn_count[rdn_count]: + self._update_record(all_updates[dn]) sortedkeys.reverse() - for k in sortedkeys: - for dn in dn_list[k]: - self.__delete_record(all_updates[dn]) + for rdn_count in sortedkeys: + for dn in dn_by_rdn_count[rdn_count]: + self._delete_record(all_updates[dn]) def update(self, files): """Execute the update. files is a list of the update files to use. @@ -796,71 +847,46 @@ class LDAPUpdate: returns True if anything was changed, otherwise False """ - updates = None + all_updates = {} if self.plugins: - root_logger.info('PRE_UPDATE') + self.info('PRE_UPDATE') updates = api.Backend.updateclient.update(PRE_UPDATE, self.dm_password, self.ldapi, self.live_run) - + self.merge_updates(all_updates, updates) try: self.create_connection() - all_updates = {} - dn_list = {} - # Start with any updates passed in from pre-update plugins - if updates: - for entry in updates: - all_updates.update(entry) - for upd in updates: - for dn in upd: - dn_explode = ldap.explode_dn(dn.lower()) - l = len(dn_explode) - if dn_list.get(l): - if dn not in dn_list[l]: - dn_list[l].append(dn) - else: - dn_list[l] = [dn] for f in files: try: - root_logger.info("Parsing file %s" % f) + self.info("Parsing update file '%s'" % f) data = self.read_file(f) except Exception, e: - print e + self.error("error reading update file '%s'", f) sys.exit(e) - (all_updates, dn_list) = self.parse_update_file(data, all_updates, dn_list) + self.parse_update_file(f, data, all_updates) - self.__run_updates(dn_list, all_updates) + self._run_updates(all_updates) finally: if self.conn: self.conn.unbind() if self.plugins: - root_logger.info('POST_UPDATE') + self.info('POST_UPDATE') + all_updates = {} updates = api.Backend.updateclient.update(POST_UPDATE, self.dm_password, self.ldapi, self.live_run) - dn_list = {} - for upd in updates: - for dn in upd: - dn_explode = ldap.explode_dn(dn.lower()) - l = len(dn_explode) - if dn_list.get(l): - if dn not in dn_list[l]: - dn_list[l].append(dn) - else: - dn_list[l] = [dn] - self.__run_updates(dn_list, updates) + self.merge_updates(all_updates, updates) + self._run_updates(all_updates) return self.modified - def update_from_dict(self, dn_list, updates): + def update_from_dict(self, updates): """ Apply updates internally as opposed to from a file. - - dn_list is a list of dns to be updated updates is a dictionary containing the updates """ if not self.conn: self.create_connection() - self.__run_updates(dn_list, updates) + self._run_updates(updates) return self.modified |