From 5dc055b54b00c40ae0fd035af4a5f524b174699d Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Mon, 23 Dec 2024 19:45:39 -0800 Subject: [PATCH] Replication suffixes and subsuffixes --- .../suites/replication/acceptance_test.py | 126 ++++++++++++++++++ src/lib389/lib389/replica.py | 10 +- 2 files changed, 131 insertions(+), 5 deletions(-) diff --git a/dirsrvtests/tests/suites/replication/acceptance_test.py b/dirsrvtests/tests/suites/replication/acceptance_test.py index f287751e10..90cc983c37 100644 --- a/dirsrvtests/tests/suites/replication/acceptance_test.py +++ b/dirsrvtests/tests/suites/replication/acceptance_test.py @@ -707,6 +707,132 @@ def test_default_cl_trimming_enabled(topo_m2): assert cl.get_attr_val_utf8("nsslapd-changelogmaxage") == "7d" +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) + """ + # Define additional suffixes + SUFFIX_2 = "dc=test2" + SUFFIX_3 = f"dc=test3,{DEFAULT_SUFFIX}" + all_suffixes = [DEFAULT_SUFFIX, SUFFIX_2, SUFFIX_3] + + # Track test users for cleanup or later checks + test_users_by_suffix = {suffix: [] for suffix in all_suffixes} + + # Get all suppliers for easier reference + suppliers = [ + topo_m4.ms["supplier1"], + topo_m4.ms["supplier2"], + topo_m4.ms["supplier3"], + topo_m4.ms["supplier4"] + ] + + # Setup additional backends and replication for all suppliers + for suffix in [SUFFIX_2, SUFFIX_3]: # DEFAULT_SUFFIX already exists + 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') + + # Enable replication + if supplier == suppliers[0]: # supplier1 is the "first" supplier + repl.create_first_supplier(supplier) + else: + repl.join_supplier(suppliers[0], supplier) + + # Create full mesh topology for this suffix + for i, supplier_i in enumerate(suppliers): + for j, supplier_j in enumerate(suppliers): + if i != j: # Don't create an agreement with itself + repl.ensure_agreement(supplier_i, supplier_j) + + # Generate test data for each suffix (add a set of users, then modify them) + 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("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 an unexpected 'cn' value: {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}'" + ) + + if __name__ == '__main__': # Run isolated # -s for DEBUG mode diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py index bee954d550..b9341032cb 100644 --- a/src/lib389/lib389/replica.py +++ b/src/lib389/lib389/replica.py @@ -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 \ @@ -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, @@ -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: @@ -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())