95ea96
From 0598ed6e1c48c6600774c53065716aad759da5e7 Mon Sep 17 00:00:00 2001
2737e7
From: Christian Heimes <cheimes@redhat.com>
2737e7
Date: Tue, 10 Jul 2018 14:03:28 +0200
2737e7
Subject: [PATCH] Handle races in replica config
2737e7
2737e7
When multiple replicas are installed in parallel, two replicas may try
2737e7
to create the cn=replica entry at the same time. This leads to a
2737e7
conflict on one of the replicas. replica_config() and
2737e7
ensure_replication_managers() now handle conflicts.
2737e7
2737e7
ipaldap now maps TYPE_OR_VALUE_EXISTS to DuplicateEntry(). The type or
2737e7
value exists exception is raised, when an attribute value or type is
2737e7
already set.
2737e7
2737e7
Fixes: https://pagure.io/freeipa/issue/7566
2737e7
Signed-off-by: Christian Heimes <cheimes@redhat.com>
2737e7
Reviewed-By: Thierry Bordaz <tbordaz@redhat.com>
2737e7
---
2737e7
 ipapython/ipaldap.py             |   5 ++
95ea96
 ipaserver/install/replication.py | 123 ++++++++++++++++++-------------
2737e7
 ipaserver/plugins/ldap2.py       |   3 +-
95ea96
 3 files changed, 79 insertions(+), 52 deletions(-)
2737e7
2737e7
diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
95ea96
index dcac32c31c670753b83f99eac7f6483fbad2b14d..53fdf4967868961effea7f3f64dfb3c0edfc75f3 100644
2737e7
--- a/ipapython/ipaldap.py
2737e7
+++ b/ipapython/ipaldap.py
95ea96
@@ -1016,7 +1016,12 @@ class LDAPClient(object):
2737e7
         except ldap.NO_SUCH_OBJECT:
2737e7
             raise errors.NotFound(reason=arg_desc or 'no such entry')
2737e7
         except ldap.ALREADY_EXISTS:
2737e7
+            # entry already exists
2737e7
             raise errors.DuplicateEntry()
2737e7
+        except ldap.TYPE_OR_VALUE_EXISTS:
2737e7
+            # attribute type or attribute value already exists, usually only
2737e7
+            # occurs, when two machines try to write at the same time.
2737e7
+            raise errors.DuplicateEntry(message=unicode(desc))
2737e7
         except ldap.CONSTRAINT_VIOLATION:
2737e7
             # This error gets thrown by the uniqueness plugin
2737e7
             _msg = 'Another entry with the same attribute value already exists'
2737e7
diff --git a/ipaserver/install/replication.py b/ipaserver/install/replication.py
95ea96
index d0f92c76c108d237104a81c72567250583ac4ff1..78c4a43cc9b8d9a6740d26209f347f64b879743e 100644
2737e7
--- a/ipaserver/install/replication.py
2737e7
+++ b/ipaserver/install/replication.py
95ea96
@@ -34,7 +34,7 @@ import ldap
95ea96
 from ipalib import api, errors
2737e7
 from ipalib.cli import textui
2737e7
 from ipalib.text import _
2737e7
-from ipapython import ipautil, ipaldap, kerberos
2737e7
+from ipapython import ipautil, ipaldap
2737e7
 from ipapython.admintool import ScriptError
2737e7
 from ipapython.dn import DN
95ea96
 from ipapython.ipaldap import ldap_initialize
95ea96
@@ -461,7 +461,7 @@ class ReplicationManager(object):
2737e7
         return DN(('cn', 'replica'), ('cn', self.db_suffix),
2737e7
                   ('cn', 'mapping tree'), ('cn', 'config'))
2737e7
 
2737e7
-    def set_replica_binddngroup(self, r_conn, entry):
2737e7
+    def _set_replica_binddngroup(self, r_conn, entry):
2737e7
         """
2737e7
         Set nsds5replicabinddngroup attribute on remote master's replica entry.
2737e7
         Older masters (ipa < 3.3) may not support setting this attribute. In
95ea96
@@ -476,11 +476,6 @@ class ReplicationManager(object):
2737e7
             mod.append((ldap.MOD_ADD, 'nsds5replicabinddngroup',
2737e7
                         self.repl_man_group_dn))
2737e7
 
2737e7
-        if 'nsds5replicabinddngroupcheckinterval' not in entry:
2737e7
-            mod.append(
2737e7
-                (ldap.MOD_ADD,
2737e7
-                 'nsds5replicabinddngroupcheckinterval',
2737e7
-                 '60'))
2737e7
         if mod:
2737e7
             try:
2737e7
                 r_conn.modify_s(entry.dn, mod)
95ea96
@@ -488,49 +483,62 @@ class ReplicationManager(object):
95ea96
                 logger.debug(
2737e7
                     "nsds5replicabinddngroup attribute not supported on "
2737e7
                     "remote master.")
2737e7
+            except (ldap.ALREADY_EXISTS, ldap.CONSTRAINT_VIOLATION):
95ea96
+                logger.debug("No update to %s necessary", entry.dn)
2737e7
 
2737e7
     def replica_config(self, conn, replica_id, replica_binddn):
2737e7
         assert isinstance(replica_binddn, DN)
2737e7
         dn = self.replica_dn()
2737e7
         assert isinstance(dn, DN)
2737e7
 
95ea96
+        logger.debug("Add or update replica config %s", dn)
2737e7
         try:
2737e7
             entry = conn.get_entry(dn)
2737e7
         except errors.NotFound:
2737e7
-            pass
2737e7
-        else:
2737e7
-            binddns = entry.setdefault('nsDS5ReplicaBindDN', [])
2737e7
-            if replica_binddn not in {DN(m) for m in binddns}:
2737e7
-                # Add the new replication manager
2737e7
-                binddns.append(replica_binddn)
2737e7
-            for key, value in REPLICA_CREATION_SETTINGS.items():
2737e7
-                entry[key] = value
2737e7
+            # no entry, create new one
2737e7
+            entry = conn.make_entry(
2737e7
+                dn,
2737e7
+                objectclass=["top", "nsds5replica", "extensibleobject"],
2737e7
+                cn=["replica"],
2737e7
+                nsds5replicaroot=[str(self.db_suffix)],
2737e7
+                nsds5replicaid=[str(replica_id)],
2737e7
+                nsds5replicatype=[self.get_replica_type()],
2737e7
+                nsds5flags=["1"],
2737e7
+                nsds5replicabinddn=[replica_binddn],
2737e7
+                nsds5replicabinddngroup=[self.repl_man_group_dn],
2737e7
+                nsds5replicalegacyconsumer=["off"],
2737e7
+                **REPLICA_CREATION_SETTINGS
2737e7
+            )
2737e7
             try:
2737e7
-                conn.update_entry(entry)
2737e7
-            except errors.EmptyModlist:
2737e7
-                pass
2737e7
+                conn.add_entry(entry)
2737e7
+            except errors.DuplicateEntry:
95ea96
+                logger.debug("Lost race against another replica, updating")
2737e7
+                # fetch entry that have been added by another replica
2737e7
+                entry = conn.get_entry(dn)
2737e7
+            else:
95ea96
+                logger.debug("Added replica config %s", dn)
2737e7
+                # added entry successfully
2737e7
+                return entry
2737e7
 
2737e7
-            self.set_replica_binddngroup(conn, entry)
2737e7
+        # either existing entry or lost race
2737e7
+        binddns = entry.setdefault('nsDS5ReplicaBindDN', [])
2737e7
+        if replica_binddn not in {DN(m) for m in binddns}:
2737e7
+            # Add the new replication manager
2737e7
+            binddns.append(replica_binddn)
2737e7
 
2737e7
-            # replication is already configured
2737e7
-            return
2737e7
+        for key, value in REPLICA_CREATION_SETTINGS.items():
2737e7
+            entry[key] = value
2737e7
 
2737e7
-        replica_type = self.get_replica_type()
2737e7
+        try:
2737e7
+            conn.update_entry(entry)
2737e7
+        except errors.EmptyModlist:
95ea96
+            logger.debug("No update to %s necessary", entry.dn)
2737e7
+        else:
95ea96
+            logger.debug("Update replica config %s", entry.dn)
2737e7
 
2737e7
-        entry = conn.make_entry(
2737e7
-            dn,
2737e7
-            objectclass=["top", "nsds5replica", "extensibleobject"],
2737e7
-            cn=["replica"],
2737e7
-            nsds5replicaroot=[str(self.db_suffix)],
2737e7
-            nsds5replicaid=[str(replica_id)],
2737e7
-            nsds5replicatype=[replica_type],
2737e7
-            nsds5flags=["1"],
2737e7
-            nsds5replicabinddn=[replica_binddn],
2737e7
-            nsds5replicabinddngroup=[self.repl_man_group_dn],
2737e7
-            nsds5replicalegacyconsumer=["off"],
2737e7
-            **REPLICA_CREATION_SETTINGS
2737e7
-        )
2737e7
-        conn.add_entry(entry)
2737e7
+        self._set_replica_binddngroup(conn, entry)
2737e7
+
2737e7
+        return entry
2737e7
 
2737e7
     def setup_changelog(self, conn):
2737e7
         ent = conn.get_entry(
95ea96
@@ -690,7 +698,10 @@ class ReplicationManager(object):
2737e7
                 uid=["passsync"],
2737e7
                 userPassword=[password],
2737e7
             )
2737e7
-            conn.add_entry(entry)
2737e7
+            try:
2737e7
+                conn.add_entry(entry)
2737e7
+            except errors.DuplicateEntry:
2737e7
+                pass
2737e7
 
2737e7
         # Add the user to the list of users allowed to bypass password policy
2737e7
         extop_dn = DN(('cn', 'ipa_pwd_extop'), ('cn', 'plugins'), ('cn', 'config'))
95ea96
@@ -1658,7 +1669,10 @@ class ReplicationManager(object):
2737e7
             objectclass=['top', 'groupofnames'],
2737e7
             cn=['replication managers']
2737e7
         )
2737e7
-        conn.add_entry(entry)
2737e7
+        try:
2737e7
+            conn.add_entry(entry)
2737e7
+        except errors.DuplicateEntry:
2737e7
+            pass
2737e7
 
2737e7
     def ensure_replication_managers(self, conn, r_hostname):
2737e7
         """
95ea96
@@ -1668,23 +1682,24 @@ class ReplicationManager(object):
2737e7
         On FreeIPA 3.x masters lacking support for nsds5ReplicaBinddnGroup
2737e7
         attribute, add replica bind DN directly into the replica entry.
2737e7
         """
2737e7
-        my_princ = kerberos.Principal((u'ldap', unicode(self.hostname)),
2737e7
-                                      realm=self.realm)
2737e7
-        remote_princ = kerberos.Principal((u'ldap', unicode(r_hostname)),
2737e7
-                                          realm=self.realm)
2737e7
-        services_dn = DN(api.env.container_service, api.env.basedn)
2737e7
-
2737e7
-        mydn, remote_dn = tuple(
2737e7
-            DN(('krbprincipalname', unicode(p)), services_dn) for p in (
2737e7
-                my_princ, remote_princ))
2737e7
+        my_dn = DN(
2737e7
+            ('krbprincipalname', u'ldap/%s@%s' % (self.hostname, self.realm)),
2737e7
+            api.env.container_service,
2737e7
+            api.env.basedn
2737e7
+        )
2737e7
+        remote_dn = DN(
2737e7
+            ('krbprincipalname', u'ldap/%s@%s' % (r_hostname, self.realm)),
2737e7
+            api.env.container_service,
2737e7
+            api.env.basedn
2737e7
+        )
2737e7
 
2737e7
         try:
2737e7
             conn.get_entry(self.repl_man_group_dn)
2737e7
         except errors.NotFound:
2737e7
-            self._add_replica_bind_dn(conn, mydn)
2737e7
+            self._add_replica_bind_dn(conn, my_dn)
2737e7
             self._add_replication_managers(conn)
2737e7
 
2737e7
-        self._add_dn_to_replication_managers(conn, mydn)
2737e7
+        self._add_dn_to_replication_managers(conn, my_dn)
2737e7
         self._add_dn_to_replication_managers(conn, remote_dn)
2737e7
 
2737e7
     def add_temp_sasl_mapping(self, conn, r_hostname):
95ea96
@@ -1704,7 +1719,10 @@ class ReplicationManager(object):
2737e7
 
2737e7
         entry = conn.get_entry(self.replica_dn())
2737e7
         entry['nsDS5ReplicaBindDN'].append(replica_binddn)
2737e7
-        conn.update_entry(entry)
2737e7
+        try:
2737e7
+            conn.update_entry(entry)
2737e7
+        except errors.EmptyModlist:
2737e7
+            pass
2737e7
 
2737e7
         entry = conn.make_entry(
2737e7
             DN(('cn', 'Peer Master'), ('cn', 'mapping'), ('cn', 'sasl'),
95ea96
@@ -1716,7 +1734,10 @@ class ReplicationManager(object):
2737e7
             nsSaslMapFilterTemplate=['(cn=&@%s)' % self.realm],
2737e7
             nsSaslMapPriority=['1'],
2737e7
         )
2737e7
-        conn.add_entry(entry)
2737e7
+        try:
2737e7
+            conn.add_entry(entry)
2737e7
+        except errors.DuplicateEntry:
2737e7
+            pass
2737e7
 
2737e7
     def remove_temp_replication_user(self, conn, r_hostname):
2737e7
         """
2737e7
diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py
95ea96
index 8745866ad11cf6248d2b9dcff560c0a5ee6d109b..5d39cdf628d7e20ce5b259d97b479ef7c8ed88d5 100644
2737e7
--- a/ipaserver/plugins/ldap2.py
2737e7
+++ b/ipaserver/plugins/ldap2.py
95ea96
@@ -419,7 +419,8 @@ class ldap2(CrudBackend, LDAPClient):
2737e7
                 modlist = [(a, b, self.encode(c))
2737e7
                            for a, b, c in modlist]
2737e7
                 self.conn.modify_s(str(group_dn), modlist)
2737e7
-        except errors.DatabaseError:
2737e7
+        except errors.DuplicateEntry:
2737e7
+            # TYPE_OR_VALUE_EXISTS
2737e7
             raise errors.AlreadyGroupMember()
2737e7
 
2737e7
     def remove_entry_from_group(self, dn, group_dn, member_attr='member'):
2737e7
-- 
2737e7
2.17.1
2737e7