358 lines
16 KiB
Diff
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
|
|
|