From 0c31776b1ea4e3725e46bce1e3d9572da02cb766 Mon Sep 17 00:00:00 2001
From: Christian Heimes <cheimes@redhat.com>
Date: Thu, 6 May 2021 10:08:43 +0200
Subject: [PATCH] Fix update_dna_shared_config to wait for both entries

update_dna_shared_config plugin now waits for presence of both
``dnaHostname=fqdn+dnaPortNum=0`` and
``dnaHostname=fqdn+dnaPortNum=389`` entries before it updates the
entries.

The commit also refactors the code of update_dna_shared_config for
subordinate id support.

Fixes: https://pagure.io/freeipa/issue/8831
Signed-off-by: Christian Heimes <cheimes@redhat.com>
---
 .../plugins/update_dna_shared_config.py       | 146 ++++++++++--------
 1 file changed, 84 insertions(+), 62 deletions(-)

diff --git a/ipaserver/install/plugins/update_dna_shared_config.py b/ipaserver/install/plugins/update_dna_shared_config.py
index 6cb47def821..70e854303d6 100644
--- a/ipaserver/install/plugins/update_dna_shared_config.py
+++ b/ipaserver/install/plugins/update_dna_shared_config.py
@@ -15,73 +15,95 @@
 
 register = Registry()
 
-
 @register()
 class update_dna_shared_config(Updater):
-    def execute(self, **options):
-        method = options.get('method', "SASL/GSSAPI")
-        protocol = options.get('protocol', "LDAP")
+    dna_plugin_names = ('posix IDs',)
 
-        dna_bind_method = "dnaRemoteBindMethod"
-        dna_conn_protocol = "dnaRemoteConnProtocol"
-        dna_plugin = DN(('cn', 'Distributed Numeric Assignment Plugin'),
-                        ('cn', 'plugins'),
-                        ('cn', 'config'))
-        dna_config_base = DN(('cn', 'posix IDs'), dna_plugin)
+    dna_plugin_dn = DN(
+        ('cn', 'Distributed Numeric Assignment Plugin'),
+        ('cn', 'plugins'),
+        ('cn', 'config')
+    )
 
-        conn = self.api.Backend.ldap2
+    def is_dna_enabled(self):
+        """Check the plugin is enabled
 
-        # Check the plugin is enabled else it is useless to update
-        # the shared entry
+        Else it is useless to update the shared entry
+        """
         try:
-            entry = conn.get_entry(dna_plugin)
-            if entry.single_value.get('nsslapd-pluginenabled') == 'off':
-                return False, ()
+            entry = self.api.Backend.ldap2.get_entry(self.dna_plugin_dn)
+            enabled = entry.single_value.get('nsslapd-pluginenabled')
+            if enabled.lower() == 'off':
+                return False
+            else:
+                return True
         except errors.NotFound:
             logger.error("Could not find DNA plugin entry: %s",
-                         dna_config_base)
-            return False, ()
+                         self.dna_plugin_dn)
+            return False
 
+    def get_shared_cfg(self, plugin_name):
+        dna_config_base = DN(('cn', plugin_name), self.dna_plugin_dn)
         try:
-            entry = conn.get_entry(dna_config_base)
+            entry = self.api.Backend.ldap2.get_entry(dna_config_base)
         except errors.NotFound:
             logger.error("Could not find DNA config entry: %s",
                          dna_config_base)
             return False, ()
+        else:
+            logger.debug('Found DNA config %s', dna_config_base)
 
         sharedcfgdn = entry.single_value.get("dnaSharedCfgDN")
         if sharedcfgdn is not None:
             sharedcfgdn = DN(sharedcfgdn)
+            logger.debug("dnaSharedCfgDN: %s", sharedcfgdn)
+            return sharedcfgdn
         else:
             logger.error(
                 "Could not find DNA shared config DN in entry: %s",
                 dna_config_base)
-            return False, ()
+            return None
+
+    def update_shared_cfg(self, sharedcfgdn, **options):
+        """Update the shared config entry related to that host
+
+        If the shared config entry already exists (like upgrade)
+        the update occurs immediately without sleep.
+
+        If the shared config entry does not exist (fresh install)
+        DS server waits for 30s after its startup to create it.
+        Startup likely occurred few sec before this function is
+        called so this loop will wait for 30s max.
+
+        In case the server is not able to create the entry
+        The loop gives a grace period of 60s before logging
+        the failure to update the shared config entry and return
+        """
+
+        method = options.get('method', "SASL/GSSAPI")
+        protocol = options.get('protocol', "LDAP")
+        dna_bind_method = "dnaRemoteBindMethod"
+        dna_conn_protocol = "dnaRemoteConnProtocol"
 
-        #
-        # Update the shared config entry related to that host
-        #
-        # If the shared config entry already exists (like upgrade)
-        # the update occurs immediately without sleep.
-        #
-        # If the shared config entry does not exist (fresh install)
-        # DS server waits for 30s after its startup to create it.
-        # Startup likely occurred few sec before this function is
-        # called so this loop will wait for 30s max.
-        #
-        # In case the server is not able to create the entry
-        # The loop gives a grace period of 60s before logging
-        # the failure to update the shared config entry and return
-        #
         max_wait = 30
+
+        conn = self.api.Backend.ldap2
         fqdn = self.api.env.host
+
         for _i in range(0, max_wait + 1):
             try:
                 entries = conn.get_entries(
                     sharedcfgdn, scope=ldap.SCOPE_ONELEVEL,
                     filter='dnaHostname=%s' % fqdn
                 )
-                break
+                # There must be two entries:
+                # - dnaHostname=fqdn+dnaPortNum=0
+                # - dnaHostname=fqdn+dnaPortNum=389
+                if len(entries) >= 2:
+                    break
+
+                logger.debug("Got only one entry. Retry again in 2 sec.")
+                time.sleep(2)
             except errors.NotFound:
                 logger.debug(
                     "Unable to find DNA shared config entry for "
@@ -94,35 +116,35 @@ def execute(self, **options):
                 "Could not get dnaHostname entries in %s seconds",
                 max_wait * 2
             )
-            return False, ()
-
-        # If there are several entries, all of them will be updated
-        # just log a debug msg. This is likely the result of #5510
-        if len(entries) != 1:
-            logger.debug(
-                "%d entries dnaHostname=%s under %s. One expected",
-                len(entries), fqdn, sharedcfgdn
-            )
+            return False
 
         # time to set the bind method and the protocol in the
         # shared config entries
         for entry in entries:
-            update = False
-            if entry.single_value.get(dna_bind_method) != method:
-                entry[dna_bind_method] = method
-                update = True
-
-            if entry.single_value.get(dna_conn_protocol) != protocol:
-                entry[dna_conn_protocol] = protocol
-                update = True
-
-            if update:
-                try:
-                    conn.update_entry(entry)
-                except Exception as e:
-                    logger.error(
-                        "Failed to set SASL/GSSAPI bind method/protocol "
-                        "in entry %s: %s", entry, e
-                    )
+            entry[dna_bind_method] = method
+            entry[dna_conn_protocol] = protocol
+            try:
+                conn.update_entry(entry)
+            except errors.EmptyModlist:
+                logger.debug("Entry %s is already updated", entry.dn)
+            except Exception as e:
+                logger.error(
+                    "Failed to set SASL/GSSAPI bind method/protocol "
+                    "in entry %s: %s", entry, e
+                )
+            else:
+                logger.debug("Updated entry %s", entry.dn)
+
+        return True
+
+    def execute(self, **options):
+        if not self.is_dna_enabled():
+            return False, ()
+
+        for plugin_name in self.dna_plugin_names:
+            sharedcfgdn = self.get_shared_cfg(plugin_name)
+            if sharedcfgdn is not None:
+                self.update_shared_cfg(sharedcfgdn, **options)
+
         # no restart, no update
         return False, ()
