389-ds-base/SOURCES/0040-Issue-6497-lib389-Configure-replication-for-multiple.patch

358 lines
16 KiB
Diff

From b812afe4da6db134c1221eb48a6155480e4c2cb3 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Tue, 14 Jan 2025 13:55:03 -0500
Subject: [PATCH] Issue 6497 - lib389 - Configure replication for multiple
suffixes (#6498)
Bug Description: When trying to set up replication across multiple suffixes -
particularly if one of those suffixes is a subsuffix - lib389 fails to properly
configure the replication agreements, service accounts, and required groups.
The references to the replication_managers group and service account
naming do not correctly account for non-default additional suffixes.
Fix Description: Ensure replication DNs and credentials are correctly tied to each suffix.
Enable DSLdapObject.present method to compare values as
a normalized DNs if they are DNs.
Add a test (test_multi_subsuffix_replication) to verify multi-suffix
replication across four suppliers.
Fix tests that are related to repl service accounts.
Fixes: https://github.com/389ds/389-ds-base/issues/6497
Reviewed: @progier389 (Thanks!)
---
.../tests/suites/ds_tools/replcheck_test.py | 4 +-
.../suites/replication/acceptance_test.py | 153 ++++++++++++++++++
.../cleanallruv_shutdown_crash_test.py | 4 +-
.../suites/replication/regression_m2_test.py | 2 +-
.../replication/tls_client_auth_repl_test.py | 4 +-
src/lib389/lib389/_mapped_object.py | 21 ++-
src/lib389/lib389/replica.py | 10 +-
7 files changed, 182 insertions(+), 16 deletions(-)
diff --git a/dirsrvtests/tests/suites/ds_tools/replcheck_test.py b/dirsrvtests/tests/suites/ds_tools/replcheck_test.py
index f61fc432d..dfa1d9423 100644
--- a/dirsrvtests/tests/suites/ds_tools/replcheck_test.py
+++ b/dirsrvtests/tests/suites/ds_tools/replcheck_test.py
@@ -67,10 +67,10 @@ def topo_tls_ldapi(topo):
# Create the replication dns
services = ServiceAccounts(m1, DEFAULT_SUFFIX)
- repl_m1 = services.get('%s:%s' % (m1.host, m1.sslport))
+ repl_m1 = services.get(f'{DEFAULT_SUFFIX}:{m1.host}:{m1.sslport}')
repl_m1.set('nsCertSubjectDN', m1.get_server_tls_subject())
- repl_m2 = services.get('%s:%s' % (m2.host, m2.sslport))
+ repl_m2 = services.get(f'{DEFAULT_SUFFIX}:{m2.host}:{m2.sslport}')
repl_m2.set('nsCertSubjectDN', m2.get_server_tls_subject())
# Check the replication is "done".
diff --git a/dirsrvtests/tests/suites/replication/acceptance_test.py b/dirsrvtests/tests/suites/replication/acceptance_test.py
index d1cfa8bdb..fc8622051 100644
--- a/dirsrvtests/tests/suites/replication/acceptance_test.py
+++ b/dirsrvtests/tests/suites/replication/acceptance_test.py
@@ -9,6 +9,7 @@
import pytest
import logging
import time
+from lib389.backend import Backend
from lib389.replica import Replicas
from lib389.tasks import *
from lib389.utils import *
@@ -325,6 +326,158 @@ def test_modify_stripattrs(topo_m4):
assert attr_value in entries[0].data['nsds5replicastripattrs']
+def test_multi_subsuffix_replication(topo_m4):
+ """Check that replication works with multiple subsuffixes
+
+ :id: ac1aaeae-173e-48e7-847f-03b9867443c4
+ :setup: Four suppliers replication setup
+ :steps:
+ 1. Create additional suffixes
+ 2. Setup replication for all suppliers
+ 3. Generate test data for each suffix (add, modify, remove)
+ 4. Wait for replication to complete across all suppliers for each suffix
+ 5. Check that all expected data is present on all suppliers
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. Success (the data is replicated everywhere)
+ """
+
+ SUFFIX_2 = "dc=test2"
+ SUFFIX_3 = f"dc=test3,{DEFAULT_SUFFIX}"
+ all_suffixes = [DEFAULT_SUFFIX, SUFFIX_2, SUFFIX_3]
+
+ test_users_by_suffix = {suffix: [] for suffix in all_suffixes}
+ created_backends = []
+
+ suppliers = [
+ topo_m4.ms["supplier1"],
+ topo_m4.ms["supplier2"],
+ topo_m4.ms["supplier3"],
+ topo_m4.ms["supplier4"]
+ ]
+
+ try:
+ # Setup additional backends and replication for the new suffixes
+ for suffix in [SUFFIX_2, SUFFIX_3]:
+ repl = ReplicationManager(suffix)
+ for supplier in suppliers:
+ # Create a new backend for this suffix
+ props = {
+ 'cn': f'userRoot_{suffix.split(",")[0][3:]}',
+ 'nsslapd-suffix': suffix
+ }
+ be = Backend(supplier)
+ be.create(properties=props)
+ be.create_sample_entries('001004002')
+
+ # Track the backend so we can remove it later
+ created_backends.append((supplier, props['cn']))
+
+ # Enable replication
+ if supplier == suppliers[0]:
+ repl.create_first_supplier(supplier)
+ else:
+ repl.join_supplier(suppliers[0], supplier)
+
+ # Create a full mesh topology for this suffix
+ for i, supplier_i in enumerate(suppliers):
+ for j, supplier_j in enumerate(suppliers):
+ if i != j:
+ repl.ensure_agreement(supplier_i, supplier_j)
+
+ # Generate test data for each suffix (add, modify, remove)
+ for suffix in all_suffixes:
+ # Create some user entries in supplier1
+ for i in range(20):
+ user_dn = f'uid=test_user_{i},{suffix}'
+ test_user = UserAccount(suppliers[0], user_dn)
+ test_user.create(properties={
+ 'uid': f'test_user_{i}',
+ 'cn': f'Test User {i}',
+ 'sn': f'User{i}',
+ 'userPassword': 'password',
+ 'uidNumber': str(1000 + i),
+ 'gidNumber': '2000',
+ 'homeDirectory': f'/home/test_user_{i}'
+ })
+ test_users_by_suffix[suffix].append(test_user)
+
+ # Perform modifications on these entries
+ for user in test_users_by_suffix[suffix]:
+ # Add some attributes
+ for j in range(3):
+ user.add('description', f'Description {j}')
+ # Replace an attribute
+ user.replace('cn', f'Modified User {user.get_attr_val_utf8("uid")}')
+ # Delete the attributes we added
+ for j in range(3):
+ try:
+ user.remove('description', f'Description {j}')
+ except Exception:
+ pass
+
+ # Wait for replication to complete across all suppliers, for each suffix
+ for suffix in all_suffixes:
+ repl = ReplicationManager(suffix)
+ for i, supplier_i in enumerate(suppliers):
+ for j, supplier_j in enumerate(suppliers):
+ if i != j:
+ repl.wait_for_replication(supplier_i, supplier_j)
+
+ # Verify that each user and modification replicated to all suppliers
+ for suffix in all_suffixes:
+ for i in range(20):
+ user_dn = f'uid=test_user_{i},{suffix}'
+ # Retrieve this user from all suppliers
+ all_user_objs = topo_m4.all_get_dsldapobject(user_dn, UserAccount)
+ # Ensure it exists in all 4 suppliers
+ assert len(all_user_objs) == 4, (
+ f"User {user_dn} not found on all suppliers. "
+ f"Found only on {len(all_user_objs)} suppliers."
+ )
+ # Check modifications: 'cn' should now be 'Modified User test_user_{i}'
+ for user_obj in all_user_objs:
+ expected_cn = f"Modified User test_user_{i}"
+ actual_cn = user_obj.get_attr_val_utf8("cn")
+ assert actual_cn == expected_cn, (
+ f"User {user_dn} has unexpected 'cn': {actual_cn} "
+ f"(expected '{expected_cn}') on supplier {user_obj._instance.serverid}"
+ )
+ # And check that 'description' attributes were removed
+ desc_vals = user_obj.get_attr_vals_utf8('description')
+ for j in range(3):
+ assert f"Description {j}" not in desc_vals, (
+ f"User {user_dn} on supplier {user_obj._instance.serverid} "
+ f"still has 'Description {j}'"
+ )
+ finally:
+ for suffix, test_users in test_users_by_suffix.items():
+ for user in test_users:
+ try:
+ if user.exists():
+ user.delete()
+ except Exception:
+ pass
+
+ for suffix in [SUFFIX_2, SUFFIX_3]:
+ repl = ReplicationManager(suffix)
+ for supplier in suppliers:
+ try:
+ repl.remove_supplier(supplier)
+ except Exception:
+ pass
+
+ for (supplier, backend_name) in created_backends:
+ be = Backend(supplier, backend_name)
+ try:
+ be.delete()
+ except Exception:
+ pass
+
+
def test_new_suffix(topo_m4, new_suffix):
"""Check that we can enable replication on a new suffix
diff --git a/dirsrvtests/tests/suites/replication/cleanallruv_shutdown_crash_test.py b/dirsrvtests/tests/suites/replication/cleanallruv_shutdown_crash_test.py
index b4b74e339..fe9955e7e 100644
--- a/dirsrvtests/tests/suites/replication/cleanallruv_shutdown_crash_test.py
+++ b/dirsrvtests/tests/suites/replication/cleanallruv_shutdown_crash_test.py
@@ -66,10 +66,10 @@ def test_clean_shutdown_crash(topology_m2):
log.info('Creating replication dns')
services = ServiceAccounts(m1, DEFAULT_SUFFIX)
- repl_m1 = services.get('%s:%s' % (m1.host, m1.sslport))
+ repl_m1 = services.get(f'{DEFAULT_SUFFIX}:{m1.host}:{m1.sslport}')
repl_m1.set('nsCertSubjectDN', m1.get_server_tls_subject())
- repl_m2 = services.get('%s:%s' % (m2.host, m2.sslport))
+ repl_m2 = services.get(f'{DEFAULT_SUFFIX}:{m2.host}:{m2.sslport}')
repl_m2.set('nsCertSubjectDN', m2.get_server_tls_subject())
log.info('Changing auth type')
diff --git a/dirsrvtests/tests/suites/replication/regression_m2_test.py b/dirsrvtests/tests/suites/replication/regression_m2_test.py
index 72d4b9f89..9c707615f 100644
--- a/dirsrvtests/tests/suites/replication/regression_m2_test.py
+++ b/dirsrvtests/tests/suites/replication/regression_m2_test.py
@@ -64,7 +64,7 @@ class _AgmtHelper:
self.binddn = f'cn={cn},cn=config'
else:
self.usedn = False
- self.cn = f'{self.from_inst.host}:{self.from_inst.sslport}'
+ self.cn = ldap.dn.escape_dn_chars(f'{DEFAULT_SUFFIX}:{self.from_inst.host}:{self.from_inst.sslport}')
self.binddn = f'cn={self.cn}, ou=Services, {DEFAULT_SUFFIX}'
self.original_state = []
self._pass = False
diff --git a/dirsrvtests/tests/suites/replication/tls_client_auth_repl_test.py b/dirsrvtests/tests/suites/replication/tls_client_auth_repl_test.py
index a00dc5b78..ca17554c7 100644
--- a/dirsrvtests/tests/suites/replication/tls_client_auth_repl_test.py
+++ b/dirsrvtests/tests/suites/replication/tls_client_auth_repl_test.py
@@ -56,10 +56,10 @@ def tls_client_auth(topo_m2):
# Create the replication dns
services = ServiceAccounts(m1, DEFAULT_SUFFIX)
- repl_m1 = services.get('%s:%s' % (m1.host, m1.sslport))
+ repl_m1 = services.get(f'{DEFAULT_SUFFIX}:{m1.host}:{m1.sslport}')
repl_m1.set('nsCertSubjectDN', m1.get_server_tls_subject())
- repl_m2 = services.get('%s:%s' % (m2.host, m2.sslport))
+ repl_m2 = services.get(f'{DEFAULT_SUFFIX}:{m2.host}:{m2.sslport}')
repl_m2.set('nsCertSubjectDN', m2.get_server_tls_subject())
# Check the replication is "done".
diff --git a/src/lib389/lib389/_mapped_object.py b/src/lib389/lib389/_mapped_object.py
index b7391d8cc..ae00c95d0 100644
--- a/src/lib389/lib389/_mapped_object.py
+++ b/src/lib389/lib389/_mapped_object.py
@@ -19,7 +19,7 @@ from lib389._constants import DIRSRV_STATE_ONLINE
from lib389._mapped_object_lint import DSLint, DSLints
from lib389.utils import (
ensure_bytes, ensure_str, ensure_int, ensure_list_bytes, ensure_list_str,
- ensure_list_int, display_log_value, display_log_data
+ ensure_list_int, display_log_value, display_log_data, is_a_dn, normalizeDN
)
# This function filter and term generation provided thanks to
@@ -292,15 +292,28 @@ class DSLdapObject(DSLogging, DSLint):
_search_ext_s(self._instance,self._dn, ldap.SCOPE_BASE, self._object_filter, attrlist=[attr, ],
serverctrls=self._server_controls, clientctrls=self._client_controls,
escapehatch='i am sure')[0]
- values = self.get_attr_vals_bytes(attr)
+ values = self.get_attr_vals_utf8(attr)
self._log.debug("%s contains %s" % (self._dn, values))
if value is None:
# We are just checking if SOMETHING is present ....
return len(values) > 0
+
+ # Otherwise, we are checking a specific value
+ if is_a_dn(value):
+ normalized_value = normalizeDN(value)
else:
- # Check if a value really does exist.
- return ensure_bytes(value).lower() in [x.lower() for x in values]
+ normalized_value = ensure_bytes(value).lower()
+
+ # Normalize each returned value depending on whether it is a DN
+ normalized_values = []
+ for v in values:
+ if is_a_dn(v):
+ normalized_values.append(normalizeDN(v))
+ else:
+ normalized_values.append(ensure_bytes(v.lower()))
+
+ return normalized_value in normalized_values
def add(self, key, value):
"""Add an attribute with a value
diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py
index 1f321972d..cd46e86d5 100644
--- a/src/lib389/lib389/replica.py
+++ b/src/lib389/lib389/replica.py
@@ -2011,7 +2011,7 @@ class ReplicationManager(object):
return repl_group
else:
try:
- repl_group = groups.get('replication_managers')
+ repl_group = groups.get(dn=f'cn=replication_managers,{self._suffix}')
return repl_group
except ldap.NO_SUCH_OBJECT:
self._log.warning("{} doesn't have cn=replication_managers,{} entry \
@@ -2035,7 +2035,7 @@ class ReplicationManager(object):
services = ServiceAccounts(from_instance, self._suffix)
# Generate the password and save the credentials
# for putting them into agreements in the future
- service_name = '{}:{}'.format(to_instance.host, port)
+ service_name = f'{self._suffix}:{to_instance.host}:{port}'
creds = password_generate()
repl_service = services.ensure_state(properties={
'cn': service_name,
@@ -2299,7 +2299,7 @@ class ReplicationManager(object):
Internal Only.
"""
- rdn = '{}:{}'.format(from_instance.host, from_instance.sslport)
+ rdn = f'{self._suffix}:{from_instance.host}:{from_instance.sslport}'
try:
creds = self._repl_creds[rdn]
except KeyError:
@@ -2499,8 +2499,8 @@ class ReplicationManager(object):
# Touch something then wait_for_replication.
from_groups = Groups(from_instance, basedn=self._suffix, rdn=None)
to_groups = Groups(to_instance, basedn=self._suffix, rdn=None)
- from_group = from_groups.get('replication_managers')
- to_group = to_groups.get('replication_managers')
+ from_group = from_groups.get(dn=f'cn=replication_managers,{self._suffix}')
+ to_group = to_groups.get(dn=f'cn=replication_managers,{self._suffix}')
change = str(uuid.uuid4())
--
2.49.0