diff options
Diffstat (limited to 'ipaserver/install')
-rw-r--r-- | ipaserver/install/dsinstance.py | 2 | ||||
-rw-r--r-- | ipaserver/install/ldapupdate.py | 144 | ||||
-rw-r--r-- | ipaserver/install/plugins/Makefile.am | 16 | ||||
-rw-r--r-- | ipaserver/install/plugins/__init__.py | 28 | ||||
-rw-r--r-- | ipaserver/install/plugins/baseupdate.py | 68 | ||||
-rw-r--r-- | ipaserver/install/plugins/rename_managed.py | 132 | ||||
-rw-r--r-- | ipaserver/install/plugins/updateclient.py | 182 | ||||
-rw-r--r-- | ipaserver/install/upgradeinstance.py | 7 |
8 files changed, 514 insertions, 65 deletions
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py index 4260579af..77fe7d06d 100644 --- a/ipaserver/install/dsinstance.py +++ b/ipaserver/install/dsinstance.py @@ -420,7 +420,7 @@ class DsInstance(service.Service): conn.unbind() def apply_updates(self): - ld = ldapupdate.LDAPUpdate(dm_password=self.dm_password, sub_dict=self.sub_dict) + ld = ldapupdate.LDAPUpdate(dm_password=self.dm_password, sub_dict=self.sub_dict, plugins=True) files = ld.get_all_files(ldapupdate.UPDATES_DIR) ld.update(files) diff --git a/ipaserver/install/ldapupdate.py b/ipaserver/install/ldapupdate.py index 34637c1ee..c396dab6b 100644 --- a/ipaserver/install/ldapupdate.py +++ b/ipaserver/install/ldapupdate.py @@ -31,6 +31,7 @@ from ipaserver import ipaldap from ipapython import entity, ipautil from ipalib import util from ipalib import errors +from ipalib import api import ldap from ldap.dn import escape_dn_chars from ipapython.ipa_log_manager import * @@ -42,6 +43,8 @@ import os import pwd import fnmatch import csv +from ipaserver.install.plugins import PRE_UPDATE, POST_UPDATE +from ipaserver.install.plugins import FIRST, MIDDLE, LAST class BadSyntax(Exception): def __init__(self, value): @@ -49,32 +52,15 @@ class BadSyntax(Exception): def __str__(self): return repr(self.value) -class IPARestart(service.Service): - """ - Restart the 389 DS service prior to performing deletions. - """ - def __init__(self, live_run=True): - """ - This class is present to provide ldapupdate the means to - restart 389 DS to apply updates prior to performing deletes. - """ - - service.Service.__init__(self, "dirsrv") - self.live_run = live_run - - def create_instance(self): - self.step("stopping directory server", self.stop) - self.step("starting directory server", self.start) - self.start_creation("Restarting IPA to initialize updates before performing deletes:") - class LDAPUpdate: def __init__(self, dm_password, sub_dict={}, live_run=True, - online=True, ldapi=False): + 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.sub_dict = sub_dict self.live_run = live_run @@ -83,6 +69,7 @@ class LDAPUpdate: self.modified = False self.online = online self.ldapi = ldapi + self.plugins = plugins self.pw_name = pwd.getpwuid(os.geteuid()).pw_name if sub_dict.get("REALM"): @@ -554,11 +541,11 @@ class LDAPUpdate: # skip this update type, it occurs in __delete_entries() return None elif utype == 'replace': - # v has the format "old:: new" + # 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 + raise BadSyntax, "bad syntax in replace, needs to be in the format old::new in %s" % v try: e.remove(old) e.append(new) @@ -708,11 +695,12 @@ class LDAPUpdate: deletes = updates.get('deleteentry', []) for d in deletes: try: - if self.live_run: - self.conn.deleteEntry(dn) - self.modified = True + 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("Deleting non-existent entry %s", 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) @@ -724,11 +712,12 @@ class LDAPUpdate: if utype == 'deleteentry': try: - if self.live_run: - self.conn.deleteEntry(dn) - self.modified = True + 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("Deleting non-existent entry %s", 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) @@ -772,16 +761,49 @@ class LDAPUpdate: else: raise RuntimeError("Offline updates are not supported.") + def __run_updates(self, dn_list, 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() + sortedkeys.sort() + for k in sortedkeys: + for dn in dn_list[k]: + self.__update_record(all_updates[dn]) + + sortedkeys.reverse() + for k in sortedkeys: + for dn in dn_list[k]: + self.__delete_record(all_updates[dn]) + def update(self, files): """Execute the update. files is a list of the update files to use. returns True if anything was changed, otherwise False """ + updates = None + if self.plugins: + logging.info('PRE_UPDATE') + updates = api.Backend.updateclient.update(PRE_UPDATE, self.dm_password, self.ldapi, self.live_run) + 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) @@ -792,40 +814,38 @@ class LDAPUpdate: (all_updates, dn_list) = self.parse_update_file(data, all_updates, dn_list) - # Process Managed Entry Updates - managed_entries = self.__update_managed_entries() - if managed_entries: - managed_entry_dns = [[m[entry]['dn'] for entry in m] for m in managed_entries] - l = len(dn_list.keys()) - - # Add Managed Entry DN's to the DN List - for dn in managed_entry_dns: - l+=1 - dn_list[l] = dn - # Add Managed Entry Updates to All Updates List - for managed_entry in managed_entries: - all_updates.update(managed_entry) - - # 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() - sortedkeys.sort() - for k in sortedkeys: - for dn in dn_list[k]: - self.__update_record(all_updates[dn]) - - # Restart 389 Directory Service - socket_name = '/var/run/slapd-%s.socket' % self.realm.replace('.','-') - iparestart = IPARestart() - iparestart.create_instance() - installutils.wait_for_open_socket(socket_name) - self.create_connection() - - sortedkeys.reverse() - for k in sortedkeys: - for dn in dn_list[k]: - self.__delete_record(all_updates[dn]) + self.__run_updates(dn_list, all_updates) finally: if self.conn: self.conn.unbind() + if self.plugins: + logging.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) + + return self.modified + + + def update_from_dict(self, 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 diff --git a/ipaserver/install/plugins/Makefile.am b/ipaserver/install/plugins/Makefile.am new file mode 100644 index 000000000..a96d0be5c --- /dev/null +++ b/ipaserver/install/plugins/Makefile.am @@ -0,0 +1,16 @@ +NULL = + +appdir = $(pythondir)/ipaserver/install +app_PYTHON = \ + __init__.py \ + baseupdate.py \ + rename_managed.py \ + updateclient.py \ + $(NULL) + +EXTRA_DIST = \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + Makefile.in diff --git a/ipaserver/install/plugins/__init__.py b/ipaserver/install/plugins/__init__.py new file mode 100644 index 000000000..49bef4df8 --- /dev/null +++ b/ipaserver/install/plugins/__init__.py @@ -0,0 +1,28 @@ +# Authors: +# Rob Crittenden <rcritten@redhat.com> +# +# Copyright (C) 2011 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/>. + +""" +Provide a separate api for updates. +""" +PRE_UPDATE = 1 +POST_UPDATE = 2 + +FIRST = 1 +MIDDLE = 2 +LAST = 4 diff --git a/ipaserver/install/plugins/baseupdate.py b/ipaserver/install/plugins/baseupdate.py new file mode 100644 index 000000000..227dc917a --- /dev/null +++ b/ipaserver/install/plugins/baseupdate.py @@ -0,0 +1,68 @@ +# Authors: +# Rob Crittenden <rcritten@redhat.com> +# +# Copyright (C) 2011 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/>. + +from ipalib import api +from ipalib import errors +from ipalib import Updater, Object +from ipaserver.install import service +from ipaserver.install.plugins import PRE_UPDATE, POST_UPDATE, MIDDLE + +class DSRestart(service.Service): + """ + Restart the 389-ds service. + """ + def __init__(self): + """ + This class is present to provide ldapupdate the means to + restart 389-ds. + """ + service.Service.__init__(self, "dirsrv") + + def create_instance(self): + self.step("stopping directory server", self.stop) + self.step("starting directory server", self.start) + self.start_creation("Restarting Directory server to apply updates") + +class update(Object): + """ + Generic object used to register all updates into a single namespace. + """ + backend_name = 'ldap2' + +api.register(update) + +class PreUpdate(Updater): + """ + Base class for updates that run prior to file processing. + """ + updatetype = PRE_UPDATE + order = MIDDLE + + def __init__(self): + super(PreUpdate, self).__init__() + +class PostUpdate(Updater): + """ + Base class for updates that run after file processing. + """ + updatetype = POST_UPDATE + order = MIDDLE + + def __init__(self): + super(PostUpdate, self).__init__() diff --git a/ipaserver/install/plugins/rename_managed.py b/ipaserver/install/plugins/rename_managed.py new file mode 100644 index 000000000..a9eed0be3 --- /dev/null +++ b/ipaserver/install/plugins/rename_managed.py @@ -0,0 +1,132 @@ +# Authors: +# Rob Crittenden <rcritten@redhat.com> +# +# Copyright (C) 2011 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/>. + +from ipaserver.install.plugins import PRE_UPDATE, POST_UPDATE, FIRST, LAST +from ipaserver.install.plugins import PRE_UPDATE, POST_UPDATE, FIRST, LAST +from ipaserver.install.plugins.baseupdate import PreUpdate, PostUpdate +from ipalib.frontend import Updater +from ipaserver.install.plugins import baseupdate +from ipalib import api, errors +from ipapython import ipautil +import ldap as _ldap + +def entry_to_update(entry): + """ + Convert an entry into a name/value pair list that looks like an update. + + An entry is a dict. + + An update is a list of name/value pairs. + """ + update = [] + for attr in entry.keys(): + if isinstance(entry[attr], list): + for i in xrange(len(entry[attr])): + update.append('%s:%s' % (str(attr), str(entry[attr][i]))) + else: + update.append('%s:%s' % (str(attr), str(entry[attr]))) + + return update + +def generate_update(ldap, deletes=False): + """ + We need to separate the deletes that need to happen from the + new entries that need to be added. + """ + suffix = ipautil.realm_to_suffix(api.env.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 + restart = False + + # If the old entries don't exist the server has already been updated. + try: + (definitions_managed_entries, truncated) = ldap.find_entries( + searchfilter, ['*'], old_definition_container, _ldap.SCOPE_ONELEVEL, normalize=False + ) + except errors.NotFound, e: + return (False, new_managed_entries) + + for entry in definitions_managed_entries: + new_definition = {} + definition_managed_entry_updates = {} + if deletes: + old_definition = {'dn': str(entry[0]), 'deleteentry': ['dn: %s' % str(entry[0])]} + old_template = str(entry[1]['managedtemplate'][0]) + definition_managed_entry_updates[old_definition['dn']] = old_definition + old_templates.append(old_template) + else: + entry[1]['managedtemplate'] = str(entry[1]['managedtemplate'][0].replace(old_template_container, sub[1] + new)) + new_definition['dn'] = str(entry[0].replace(old_definition_container, sub[0] + new)) + new_definition['default'] = entry_to_update(entry[1]) + definition_managed_entry_updates[new_definition['dn']] = new_definition + new_managed_entries.append(definition_managed_entry_updates) + for old_template in old_templates: # Only happens when deletes is True + try: + (dn, template) = ldap.get_entry(old_template, ['*'], normalize=False) + dn = str(dn) + new_template = {} + template_managed_entry_updates = {} + old_template = {'dn': dn, 'deleteentry': ['dn: %s' % dn]} + new_template['dn'] = str(dn.replace(old_template_container, sub[1] + new)) + new_template['default'] = entry_to_update(template) + 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: + restart = True + new_managed_entries.sort(reverse=True) + + return (restart, new_managed_entries) + +class update_managed_post_first(PreUpdate): + """ + Update managed entries + """ + order=FIRST + + def execute(self, **options): + # Never need to restart with the pre-update changes + (ignore, new_managed_entries) = generate_update(self.obj.backend, False) + + return (False, True, new_managed_entries) + +api.register(update_managed_post_first) + +class update_managed_post(PostUpdate): + """ + Update managed entries + """ + order=LAST + + def execute(self, **options): + (restart, new_managed_entries) = generate_update(self.obj.backend, True) + + return (restart, True, new_managed_entries) + +api.register(update_managed_post) diff --git a/ipaserver/install/plugins/updateclient.py b/ipaserver/install/plugins/updateclient.py new file mode 100644 index 000000000..8f463fa69 --- /dev/null +++ b/ipaserver/install/plugins/updateclient.py @@ -0,0 +1,182 @@ +# Authors: Rob Crittenden <rcritten@redhat.com> +# +# Copyright (C) 2011 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/>. +# + +import os +from ipaserver.install import installutils +from ipaserver.install.plugins import FIRST, MIDDLE, LAST +from ipaserver.install.plugins import POST_UPDATE +from ipaserver.install.plugins.baseupdate import DSRestart +from ipaserver.install.ldapupdate import LDAPUpdate +from ipalib import api +from ipalib import backend +import ldap as _ldap + +class updateclient(backend.Executioner): + """ + Backend used for applying LDAP updates via plugins + + An update plugin can be executed before the file-based plugins or + afterward. Each plugin returns three values: + + 1. restart: dirsrv needs to be restarted BEFORE this update is + applied. + 2. apply_now: when True the update is applied when the plugin + returns. Otherwise the update is cached until all + plugins of that update type are complete, then they + are applied together. + 3. updates: A dictionary of updates to be applied. + + updates is a dictionary keyed on dn. The value of an update is a + dictionary with the following possible values: + - dn: str, duplicate of the key + - updates: list of updates against the dn + - default: list of the default entry to be added if it doesn't + exist + - deleteentry: list of dn's to be deleted (typically single dn) + + For example, this update file: + + dn: cn=global_policy,cn=$REALM,cn=kerberos,$SUFFIX + replace:krbPwdLockoutDuration:10::600 + replace: krbPwdMaxFailure:3::6 + + Generates this update dictionary: + + dict('cn=global_policy,cn=EXAMPLE.COM,cn=kerberos,dc=example,dc=com': + dict( + 'dn': 'cn=global_policy,cn=EXAMPLE.COM,cn=kerberos,dc=example,dc=com', + 'updates': ['replace:krbPwdLockoutDuration:10::600', + 'replace:krbPwdMaxFailure:3::6'] + ) + ) + + Here is another example showing how a default entry is configured: + + dn: cn=Managed Entries,cn=etc,$SUFFIX + default: objectClass: nsContainer + default: objectClass: top + default: cn: Managed Entries + + This generates: + + dict('cn=Managed Entries,cn=etc,dc=example,dc=com', + dict( + 'dn': 'cn=Managed Entries,cn=etc,dc=example,dc=com', + 'default': ['objectClass:nsContainer', + 'objectClass:top', + 'cn:Managed Entries' + ] + ) + ) + + Note that the variable substitution in both examples has been completed. + + A PRE_UPDATE plugin is executed before file-based updates. + + A POST_UPDATE plugin is executed after file-based updates. + + Plugins are executed automatically when ipa-ldap-updater is run + in upgrade mode (--upgrade). They are not executed normally otherwise. + To execute plugins as well use the --plugins flag. + + Either may make changes directly in LDAP or can return updates in + update format. + """ + def create_context(self, dm_password): + if dm_password: + autobind = False + else: + autobind = True + self.Backend.ldap2.connect(bind_dn='cn=Directory Manager', bind_pw=dm_password, autobind=autobind) + + def order(self, updatetype): + """ + Calculate rough order of plugins. + """ + order = [] + for plugin in api.Updater(): #pylint: disable=E1101 + if plugin.updatetype != updatetype: + continue + if plugin.order == FIRST: + order.insert(0, plugin) + elif plugin.order == MIDDLE: + order.insert(len(order)/2, plugin) + else: + order.append(plugin) + + for o in order: + yield o + + def update(self, updatetype, dm_password, ldapi, live_run): + """ + Execute all update plugins of type updatetype. + """ + self.create_context(dm_password) + kw = dict(live_run=live_run) + result = [] + ld = LDAPUpdate(dm_password=dm_password, sub_dict={}, live_run=live_run, ldapi=ldapi) + for update in self.order(updatetype): + (restart, apply_now, res) = self.run(update.name, **kw) + if restart: + self.restart(dm_password, live_run) + dn_list = {} + for upd in res: + 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] + updates = {} + for entry in res: + updates.update(entry) + + if apply_now: + ld.update_from_dict(dn_list, updates) + elif res: + result.extend(res) + + self.destroy_context() + + return result + + def run(self, method, **kw): + """ + Execute the update plugin. + """ + return self.Updater[method](**kw) #pylint: disable=E1101 + + def restart(self, dm_password, live_run): + if os.getegid() != 0: + self.log.warn("Not root, skipping restart") + return + dsrestart = DSRestart() + socket_name = '/var/run/slapd-%s.socket' % \ + api.env.realm.replace('.','-') + if live_run: + self.destroy_context() + dsrestart.create_instance() + installutils.wait_for_open_socket(socket_name) + self.create_context(dm_password) + else: + self.log.warn("Test mode, skipping restart") + +api.register(updateclient) diff --git a/ipaserver/install/upgradeinstance.py b/ipaserver/install/upgradeinstance.py index 72636497c..0913fdb45 100644 --- a/ipaserver/install/upgradeinstance.py +++ b/ipaserver/install/upgradeinstance.py @@ -21,6 +21,7 @@ import os import sys import shutil import random +import traceback from ipapython.ipa_log_manager import * from ipaserver.install import installutils @@ -100,11 +101,12 @@ class IPAUpgrade(service.Service): def __upgrade(self): try: - ld = ldapupdate.LDAPUpdate(dm_password='', ldapi=True, live_run=self.live_run) + ld = ldapupdate.LDAPUpdate(dm_password='', ldapi=True, live_run=self.live_run, plugins=True) if len(self.files) == 0: self.files = ld.get_all_files(ldapupdate.UPDATES_DIR) self.modified = ld.update(self.files) - except ldapupdate.BadSyntax: + except ldapupdate.BadSyntax, e: + logging.error('Bad syntax in upgrade %s' % str(e)) self.modified = False self.badsyntax = True except Exception, e: @@ -112,6 +114,7 @@ class IPAUpgrade(service.Service): self.modified = False self.upgradefailed = True root_logger.error('Upgrade failed with %s' % str(e)) + root_logger.debug('%s', traceback.format_exc()) def main(): if os.getegid() != 0: |