[Freeipa-devel] [PATCH] Make the migration plugin more configurable

Jakub Hrozek jhrozek at redhat.com
Wed Dec 1 12:25:23 UTC 2010


On Wed, Nov 24, 2010 at 04:54:19PM -0500, Rob Crittenden wrote:
> Jakub Hrozek wrote:
> >-----BEGIN PGP SIGNED MESSAGE-----
> >Hash: SHA1
> >
> >On 11/22/2010 04:21 PM, Jakub Hrozek wrote:
> >>On 11/22/2010 04:16 PM, Jakub Hrozek wrote:
> >>>The code handles it (I just ran a quick test with --schema=RFC2307bis).
> >>
> >>>It just iterates through all members of a group -- be it user member of
> >>>group member, it's just a DN for the plugin.
> >>
> >>>	Jakub
> >>
> >>Sorry, I found another bug in the plugin. I'll send a new patch shortly,
> >>so please don't waste time reviewing this one.
> >
> >New patch is attached. It fixes two more bugs of the original plugin -
> >determines whether a group member is a user or a nested group by
> >checking the DN, not just the RDN attribute name and does not hardcode
> >primary keys.
> 
> Will this blow up in convert_members_rfc2307bis() if a member isn't
> contained in the users and groups containers? Should there be a
> failsafe to skip over things that don't match (along with
> appropriate reporting)?

It wouldn't blow up but add the original DN into the member attribute
which is probably worse. Thanks for catching this. I modified the patch
to log all migrated users and groups with info() and skip those that
don't match any of the containers while logging these entries with
error().

> Or if one of users or groups search bases
> isn't provided?
> 

If one of them isn't provided, a default would be used.

> It definitely doesn't like this:
> # ipa migrate-ds --user-container=''
> --group-container='cn=groups,cn=accounts' ldap://ds.example.com:389
> 
> When passed the right set of options it does seem to do the right thing.
> 

Sorry, but I don't quite understand the "--user-container=''" switch.
Does it mean the users are rooted at the Base DN? Can you post the error
or relevant log info? Please note that the default objectclass is
person.
-------------- next part --------------
>From 1b0f43c4449bd26ffe6c57a594f3eaf367cda2c4 Mon Sep 17 00:00:00 2001
From: Jakub Hrozek <jhrozek at redhat.com>
Date: Tue, 26 Oct 2010 16:10:42 -0400
Subject: [PATCH] Make the migration plugin more configurable

This patch adds new options to the migration plugin:
 * the option to fine-tune the objectclass of users or groups being imported
 * the option to select the LDAP schema (RFC2307 or RFC2307bis)

Also makes the logic that decides whether an entry is a nested group or user
(for RFC2307bis) smarter by looking at the DNS. Does not hardcode primary keys
for migrated entries.

https://fedorahosted.org/freeipa/ticket/429
---
 ipalib/plugins/migration.py |  136 ++++++++++++++++++++++++++++++++++---------
 1 files changed, 108 insertions(+), 28 deletions(-)

diff --git a/ipalib/plugins/migration.py b/ipalib/plugins/migration.py
index 6dc9934..213c0ee 100644
--- a/ipalib/plugins/migration.py
+++ b/ipalib/plugins/migration.py
@@ -26,9 +26,10 @@ Example: Migrate users and groups from DS to IPA
 
 import logging
 import re
+import ldap as _ldap
 
 from ipalib import api, errors, output
-from ipalib import Command, List, Password, Str, Flag
+from ipalib import Command, List, Password, Str, Flag, StrEnum
 from ipalib.cli import to_cli
 if api.env.in_server and api.env.context in ['lite', 'server']:
     try:
@@ -44,8 +45,10 @@ from ipalib.text import Gettext # FIXME: remove once the other Gettext FIXME is
 _krb_err_msg = _('Kerberos principal %s already exists. Use \'ipa user-mod\' to set it manually.')
 _grp_err_msg = _('Failed to add user to the default group. Use \'ipa group-add-member\' to add manually.')
 
+_supported_schemas = (u'RFC2307bis', u'RFC2307')
 
-def _pre_migrate_user(ldap, pkey, dn, entry_attrs, failed, config, ctx):
+
+def _pre_migrate_user(ldap, pkey, dn, entry_attrs, failed, config, ctx, **kwargs):
     # get default primary group for new users
     if 'def_group_dn' not in ctx:
         def_group = config.get('ipadefaultprimarygroup')
@@ -90,37 +93,80 @@ def _post_migrate_user(ldap, pkey, dn, entry_attrs, failed, config, ctx):
 
 # GROUP MIGRATION CALLBACKS AND VARS
 
-def _pre_migrate_group(ldap, pkey, dn, entry_attrs, failed, config, ctx):
-    def convert_members(member_attr, overwrite=False):
+def _pre_migrate_group(ldap, pkey, dn, entry_attrs, failed, config, ctx, **kwargs):
+    def convert_members_rfc2307bis(member_attr, search_bases, overwrite=False):
         """
         Convert DNs in member attributes to work in IPA.
         """
         new_members = []
         entry_attrs.setdefault(member_attr, [])
         for m in entry_attrs[member_attr]:
-            col = m.find(',')
-            if col == -1:
+            try:
+                # what str2dn returns looks like [[('cn', 'foo', 4)], [('dc', 'example', 1)], [('dc', 'com', 1)]]
+                rdn = _ldap.dn.str2dn(m ,flags=_ldap.DN_FORMAT_LDAPV3)[0]
+                rdnval = rdn[0][1]
+            except IndexError:
+                api.log.error('Malformed DN %s has no RDN?' % m)
+                continue
+
+            if m.lower().endswith(search_bases['user']):
+                api.log.info('migrating user %s' % m)
+                m = '%s=%s,%s' % (api.Object.user.primary_key.name,
+                                  rdnval,
+                                  api.env.container_user)
+            elif m.lower().endswith(search_bases['group']):
+                api.log.info('migrating group %s' % m)
+                m = '%s=%s,%s' % (api.Object.group.primary_key.name,
+                                  rdnval,
+                                  api.env.container_group)
+            else:
+                api.log.error('entry %s does not belong into any known container' % m)
                 continue
-            if m.startswith('uid'):
-                m = '%s,%s' % (m[0:col], api.env.container_user)
-            elif m.startswith('cn'):
-                m = '%s,%s' % (m[0:col], api.env.container_group)
+
             m = ldap.normalize_dn(m)
             new_members.append(m)
+
         del entry_attrs[member_attr]
         if overwrite:
             entry_attrs['member'] = []
         entry_attrs['member'] += new_members
 
+    def convert_members_rfc2307(member_attr):
+        """
+        Convert usernames in member attributes to work in IPA.
+        """
+        new_members = []
+        entry_attrs.setdefault(member_attr, [])
+        for m in entry_attrs[member_attr]:
+            memberdn = '%s=%s,%s' % (api.Object.user.primary_key.name,
+                                     m,
+                                     api.env.container_user)
+            new_members.append(ldap.normalize_dn(memberdn))
+        entry_attrs['member'] = new_members
+
+    schema = kwargs.get('schema', None)
     entry_attrs['ipauniqueid'] = 'autogenerate'
-    convert_members('member', overwrite=True)
-    convert_members('uniquemember')
+    if schema == 'RFC2307bis':
+        search_bases = kwargs.get('search_bases', None)
+        if not search_bases:
+            raise ValueError('Search bases not specified')
+
+        convert_members_rfc2307bis('member', search_bases, overwrite=True)
+        convert_members_rfc2307bis('uniquemember', search_bases)
+    elif schema == 'RFC2307':
+        convert_members_rfc2307('memberuid')
+    else:
+        raise ValueError('Schema %s not supported' % schema)
 
     return dn
 
 
 # DS MIGRATION PLUGIN
 
+def construct_filter(template, oc_list):
+    oc_subfilter = ''.join([ '(objectclass=%s)' % oc for oc in oc_list])
+    return template % oc_subfilter
+
 def validate_ldapuri(ugettext, ldapuri):
     m = re.match('^ldaps?://[-\w\.]+(:\d+)?$', ldapuri)
     if not m:
@@ -152,14 +198,18 @@ class migrate_ds(Command):
         #
         # If pre_callback return value evaluates to False, migration
         # of the current object is aborted.
-        'user': (
-            '(&(objectClass=person)(uid=*))',
-            _pre_migrate_user, _post_migrate_user
-        ),
-        'group': (
-            '(&(|(objectClass=groupOfUniqueNames)(objectClass=groupOfNames))(cn=*))',
-            _pre_migrate_group, None
-        ),
+        'user': {
+            'filter_template' : '(&(|%s)(uid=*))',
+            'oc_option' : 'userobjectclass',
+            'pre_callback' : _pre_migrate_user,
+            'post_callback' : _post_migrate_user
+        },
+        'group': {
+            'filter_template' : '(&(|%s)(cn=*))',
+            'oc_option' : 'groupobjectclass',
+            'pre_callback' : _pre_migrate_group,
+            'post_callback' : None
+        },
     }
     migrate_order = ('user', 'group')
 
@@ -196,6 +246,28 @@ class migrate_ds(Command):
             default=u'ou=groups',
             autofill=True,
         ),
+        List('userobjectclass?',
+            cli_name='user_objectclass',
+            label=_('User object class'),
+            doc=_('Comma-separated list of objectclasses used to search for user entries in DS'),
+            default=(u'person',),
+            autofill=True,
+        ),
+        List('groupobjectclass?',
+            cli_name='group_objectclass',
+            label=_('Group object class'),
+            doc=_('Comma-separated list of objectclasses used to search for group entries in DS'),
+            default=(u'groupOfUniqueNames', u'groupOfNames'),
+            autofill=True,
+        ),
+        StrEnum('schema?',
+            cli_name='schema',
+            label=_('LDAP schema'),
+            doc=_('The schema used on the LDAP server. Supported values are RFC2307 and RFC2307bis. The default is RFC2307bis'),
+            values=_supported_schemas,
+            default=_supported_schemas[0],
+            autofill=True,
+        ),
         Flag('continue?',
             doc=_('Continous operation mode. Errors are reported but the process continues'),
             default=False,
@@ -267,19 +339,26 @@ can use their Kerberos accounts.''')
                 else:
                     options[p.name] = tuple()
 
+    def _get_search_bases(self, options, ds_base_dn, migrate_order):
+        search_bases = dict()
+        for ldap_obj_name in migrate_order:
+            search_bases[ldap_obj_name] = '%s,%s' % (
+                options['%scontainer' % to_cli(ldap_obj_name)], ds_base_dn
+            )
+        return search_bases
+
     def migrate(self, ldap, config, ds_ldap, ds_base_dn, options):
         """
         Migrate objects from DS to LDAP.
         """
         migrated = {} # {'OBJ': ['PKEY1', 'PKEY2', ...], ...}
         failed = {} # {'OBJ': {'PKEY1': 'Failed 'cos blabla', ...}, ...}
+        search_bases = self._get_search_bases(options, ds_base_dn, self.migrate_order)
         for ldap_obj_name in self.migrate_order:
             ldap_obj = self.api.Object[ldap_obj_name]
 
-            search_filter = self.migrate_objects[ldap_obj_name][0]
-            search_base = '%s,%s' % (
-                options['%scontainer' % to_cli(ldap_obj_name)], ds_base_dn
-            )
+            search_filter = construct_filter(self.migrate_objects[ldap_obj_name]['filter_template'],
+                                             options[to_cli(self.migrate_objects[ldap_obj_name]['oc_option'])])
             exclude = options['exclude_%ss' % to_cli(ldap_obj_name)]
             context = {}
 
@@ -289,7 +368,7 @@ can use their Kerberos accounts.''')
             # FIXME: with limits set, we get a strange 'Success' exception
             try:
                 (entries, truncated) = ds_ldap.find_entries(
-                    search_filter, ['*'], search_base, ds_ldap.SCOPE_ONELEVEL#,
+                    search_filter, ['*'], search_bases[ldap_obj_name], ds_ldap.SCOPE_ONELEVEL#,
                     #time_limit=0, size_limit=0
                 )
             except errors.NotFound:
@@ -319,11 +398,12 @@ can use their Kerberos accounts.''')
                     )
                 )
 
-                callback = self.migrate_objects[ldap_obj_name][1]
+                callback = self.migrate_objects[ldap_obj_name]['pre_callback']
                 if callable(callback):
                     dn = callback(
                         ldap, pkey, dn, entry_attrs, failed[ldap_obj_name],
-                        config, context
+                        config, context, schema = options['schema'],
+                        search_bases = search_bases
                     )
                     if not dn:
                         continue
@@ -335,7 +415,7 @@ can use their Kerberos accounts.''')
                 else:
                     migrated[ldap_obj_name].append(pkey)
 
-                    callback = self.migrate_objects[ldap_obj_name][2]
+                    callback = self.migrate_objects[ldap_obj_name]['post_callback']
                     if callable(callback):
                         callback(
                             ldap, pkey, dn, entry_attrs, failed[ldap_obj_name],
-- 
1.7.3.2



More information about the Freeipa-devel mailing list