Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
# Authors: Rob Crittenden <rcritten@redhat.com> # # Copyright (C) 2008 Red Hat # see file 'COPYING' for use and warranty information # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. #
# Documentation can be found at http://freeipa.org/page/LdapUpdate
# TODO # save undo files?
return repr(self.value)
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 """
self.realm = sub_dict["REALM"] else: except krbV.Krb5Error: self.realm = None suffix = None
raise RuntimeError("Unable to determine hostname") fqdn = "ldapi://%%2fvar%%2frun%%2fslapd-%s.socket" % "-".join( self.realm.split(".") )
self.sub_dict["DOMAIN"] = domain
# Try out the connection/password elif os.getegid() == 0: try: # autobind conn.do_external_bind(self.pw_name) except errors.NotFound: # Fall back conn.do_sasl_gssapi_bind() else: conn.do_sasl_gssapi_bind() except (ldap.CONNECT_ERROR, ldap.SERVER_DOWN): raise RuntimeError("Unable to connect to LDAP server %s" % fqdn) except ldap.INVALID_CREDENTIALS: raise RuntimeError("The password provided is incorrect for LDAP server %s" % fqdn) except ldap.LOCAL_ERROR, e: raise RuntimeError('%s' % e.args[0].get('info', '').strip()) else: raise RuntimeError("Offline updates are not supported.")
# The following 2 functions were taken from the Python # documentation at http://docs.python.org/library/csv.html
# csv.py doesn't do Unicode; encode temporarily as UTF-8: dialect=dialect, delimiter=',', quotechar=quote_char, skipinitialspace=True, **kwargs) # decode UTF-8 back to Unicode, cell by cell:
"""On multi-arch systems some libraries may be in /lib64, /usr/lib64, etc. Determine if a suffix is needed based on the current architecture. """
else: return ""
except KeyError, e: raise BadSyntax("Unknown template keyword %s" % e)
"""Parse a comma-separated string into separate values and convert them into a list. This should handle quoted-strings with embedded commas """ quote_char = "'" else:
fd = sys.stdin else:
"""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. """ entry[key] = ''
"""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. """ if dn not in dn_list[l]: dn_list[l].append(dn) else:
e = all_updates[dn] if 'default' in update: if 'default' in e: e['default'] = e['default'] + update['default'] else: e['default'] = update['default'] elif 'updates' in update: if 'updates' in e: e['updates'] = e['updates'] + update['updates'] else: e['updates'] = update['updates'] else: root_logger.debug("Unknown key in updates %s" % update.keys())
all_updates[dn] = e
return all_updates
"""Parse the update file into a dictonary of lists and apply the update for each DN in the file.""" # Strip out \n and extra white space
# skip comments and empty lines
else: raise BadSyntax, "dn is not defined in the update"
v = d[len(d) - 1] v = v + line[1:] d[len(d) - 1] = v update[index] = d continue
new_value = attr + ":" + value else:
"""Create a task to update an index for an attribute"""
# Sleep a bit to ensure previous operations are complete if self.live_run: time.sleep(5)
r = random.SystemRandom()
# Refresh the time to make uniqueness more probable. Add on some # randomness for good measure. self.sub_dict['TIME'] = int(time.time()) + r.randint(0,10000)
cn = self._template_str("indextask_$TIME") dn = "cn=%s, cn=index, cn=tasks, cn=config" % cn
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)
if self.live_run: self.conn.addEntry(e)
return dn
"""Give a task DN monitor it and wait until it has completed (or failed) """
if not self.live_run: # If not doing this live there is nothing to monitor return
# Pause for a moment to give the task time to be created time.sleep(1)
attrlist = ['nstaskstatus', 'nstaskexitcode'] entry = None
while True: try: entry = self.conn.getEntry(dn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist) except errors.NotFound, e: root_logger.error("Task not found: %s", dn) return except errors.DatabaseError, e: root_logger.error("Task lookup failure %s", e) return
status = entry.getValue('nstaskstatus') if status is None: # task doesn't have a status yet time.sleep(1) continue
if status.lower().find("finished") > -1: root_logger.info("Indexing finished") break
root_logger.debug("Indexing in progress") time.sleep(1)
return
"""Create the default entry from the values provided.
The return type is entity.Entity """
# This means that the entire entry needs to be created with add
for line in default: # We already do syntax-parsing so this is safe (k, v) = line.split(':',1) e = entry.getValues(k) if e: # multi-valued attribute e = list(e) e.append(v) else: e = v entry.setValues(k, e)
return self.__entry_to_entity(entry)
"""Retrieve an object from LDAP.
The return type is ipaldap.Entry """
"""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
"""updates is a list of changes to apply entry is the thing to apply them to
Returns the modified entry """ return entry
# We already do syntax-parsing so this is safe
else: # Remove it, ignoring errors so we can blindly add it later root_logger.debug("addifnew: '%s' to %s, current value %s", v, k, e) # 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) root_logger.debug("addifexist: '%s' to %s, current value %s", v, k, e) # 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) e.append(v) else: # skip this update type, it occurs in __delete_entries() elif utype == 'replace': # v has the format "old::new" try: (old, new) = v.split('::', 1) except ValueError: raise BadSyntax, "bad syntax in replace, needs to be in the format old::new in %s" % v try: e.remove(old) e.append(new) root_logger.debug('replace: updated value %s', e) entry.setValues(k, e) except ValueError: root_logger.debug('replace: %s not found, skipping', old)
"""The entity object currently lacks a str() method""" else:
"""Compare the schema in 's' with the current schema in the DS to see if anything has changed. This should account for syntax differences (like added parens that make no difference but are detected as a change by generateModList()).
This doesn't handle re-ordering of attributes. They are still detected as changes, so foo $ bar != bar $ foo.
return True if the schema has changed return False if it has not """ signature = inspect.getargspec(ldap.schema.SubSchema.__init__) if 'check_uniqueness' in signature.args: s = ldap.schema.SubSchema(s, check_uniqueness=0) else: s = ldap.schema.SubSchema(s) 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 = n.ldap_entry()
if s == n: return False else: return True
update.get('default'))
# we should only ever get back one entry raise BadSyntax, "More than 1 entry returned on a dn search!? %s" % new_entry.dn # Doesn't exist, start with the default entry 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)
# Bring this entry up to date # It might be None if it is just deleting an entry
# New entries get their orig_data set to the entry itself. We want to # empty that so that everything appears new when generating the # modlist # entry.orig_data = {} # addifexist may result in an entry with only a # dn defined. In that case there is nothing to do. # It means the entry doesn't exist, so skip it. except Exception, e: root_logger.error("Add failure %s", e) else: # Update LDAP updated = self.is_schema_updated(entry.toDict()) else: except errors.EmptyModlist: root_logger.info("Entry already up-to-date") updated = False except errors.DatabaseError, e: root_logger.error("Update failed: %s", e) updated = False except errors.ACIError, e: root_logger.error("Update failed: %s", e) updated = False
"cn=userRoot" in entry.dn): taskid = self.create_index_task(entry.cn) self.monitor_index_task(taskid)
""" Run through all the updates again looking for any that should be deleted.
This must use a reversed list so that the longest entries are considered first so we don't end up trying to delete a parent and child in the wrong order. """ 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)
# We already do syntax-parsing so this is safe
except errors.DatabaseError, e: root_logger.error("Delete failed: %s", e)
"""Get all update files""" f = [] for path, subdirs, files in os.walk(root): for name in files: if fnmatch.fnmatch(name, "*.update"): f.append(os.path.join(path, name)) if not recursive: break f.sort() return f
self.conn = ipaldap.IPAdmin(ldapi=True, realm=self.realm) else: ldapi=False, realm=self.realm) elif os.getegid() == 0: try: # autobind self.conn.do_external_bind(self.pw_name) except errors.NotFound: # Fall back self.conn.do_sasl_gssapi_bind() else: self.conn.do_sasl_gssapi_bind() except ldap.LOCAL_ERROR, e: raise RuntimeError('%s' % e.args[0].get('info', '').strip()) else: raise RuntimeError("Offline updates are not supported.")
# For adds and updates we want to apply updates from shortest # to greatest length of the DN. For deletes we want the reverse.
"""Execute the update. files is a list of the update files to use.
returns True if anything was changed, otherwise False """
root_logger.info('PRE_UPDATE') updates = api.Backend.updateclient.update(PRE_UPDATE, self.dm_password, self.ldapi, self.live_run)
# Start with any updates passed in from pre-update plugins 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]
except Exception, e: print e sys.exit(1)
finally:
root_logger.info('POST_UPDATE') 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)
""" 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)
return self.modified |