Skip to content

Commit

Permalink
Issue 6497 - lib389 - Configure replication for multiple suffixes
Browse files Browse the repository at this point in the history
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: 389ds#6497

Reviewed: ?
  • Loading branch information
droideck committed Jan 14, 2025
1 parent f2abeb0 commit ab997bc
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 17 deletions.
4 changes: 2 additions & 2 deletions dirsrvtests/tests/suites/ds_tools/replcheck_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down
153 changes: 153 additions & 0 deletions dirsrvtests/tests/suites/replication/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down Expand Up @@ -324,6 +325,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion dirsrvtests/tests/suites/replication/regression_m2_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def __init__(self, from_inst, to_inst, cn = None):
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down
2 changes: 1 addition & 1 deletion dirsrvtests/tests/suites/tls/tls_repl_clientauth_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def topo_tls_ldapi(topo):
# Create replication DNs
services = ServiceAccounts(m1, DEFAULT_SUFFIX)
for instance in (m1, m2):
repl = services.get(f'{instance.host}:{instance.sslport}')
repl = services.get(f'{DEFAULT_SUFFIX}:{instance.host}:{instance.sslport}')
repl.set('nsCertSubjectDN', instance.get_server_tls_subject())

# Check the replication is "done".
Expand Down
21 changes: 17 additions & 4 deletions src/lib389/lib389/_mapped_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
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
Expand Down Expand Up @@ -295,15 +295,28 @@ def present(self, attr, value=None):
_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
Expand Down
10 changes: 5 additions & 5 deletions src/lib389/lib389/replica.py
Original file line number Diff line number Diff line change
Expand Up @@ -2013,7 +2013,7 @@ def _create_service_group(self, from_instance):
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 \
Expand All @@ -2037,7 +2037,7 @@ def _create_service_account(self, from_instance, to_instance):
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,
Expand Down Expand Up @@ -2296,7 +2296,7 @@ def _get_replica_creds(self, from_instance, write_instance):
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:
Expand Down Expand Up @@ -2537,8 +2537,8 @@ def wait_for_replication(self, from_instance, to_instance, timeout=60):
# 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())

Expand Down

0 comments on commit ab997bc

Please sign in to comment.