public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/389-ds-base] f43: Bump version to 3.1.5
@ 2026-09-07 17:29 Viktor Ashirov
0 siblings, 0 replies; only message in thread
From: Viktor Ashirov @ 2026-09-07 17:29 UTC (permalink / raw)
To: git-commits
A new commit has been pushed.
Repo : rpms/389-ds-base
Branch : f43
Commit : 666059dd015bd4bc0c65066c014b700a2bd05e52
Author : Viktor Ashirov <vashirov@redhat.com>
Date : 2026-09-07T16:11:52+02:00
Stats : +279/-10115 in 34 file(s)
URL : https://src.fedoraproject.org/rpms/389-ds-base/c/666059dd015bd4bc0c65066c014b700a2bd05e52?branch=f43
Log:
Bump version to 3.1.5
---
diff --git a/0001-Issue-7150-Compressed-access-log-rotations-skipped-a.patch b/0001-Issue-7150-Compressed-access-log-rotations-skipped-a.patch
deleted file mode 100644
index 7959885..0000000
--- a/0001-Issue-7150-Compressed-access-log-rotations-skipped-a.patch
+++ /dev/null
@@ -1,551 +0,0 @@
-From 045fe1a6899b7e4588be7101e81bb78995a713b1 Mon Sep 17 00:00:00 2001
-From: Simon Pichugin <spichugi@redhat.com>
-Date: Tue, 16 Dec 2025 15:48:35 -0800
-Subject: [PATCH] Issue 7150 - Compressed access log rotations skipped,
- accesslog-list out of sync (#7151)
-
-Description: Accept `.gz`-suffixed rotated log filenames when
-rebuilding rotation info and checking previous logs, preventing
-compressed rotations from being dropped from the internal list.
-
-Add regression tests to stress log rotation with compression,
-verify `nsslapd-accesslog-list` stays in sync, and guard against
-crashes when flushing buffered logs during rotation.
-Minor doc fix in test.
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7150
-
-Reviewed by: @progier389 (Thanks!)
----
- .../suites/logging/log_flush_rotation_test.py | 341 +++++++++++++++++-
- ldap/servers/slapd/log.c | 99 +++--
- 2 files changed, 402 insertions(+), 38 deletions(-)
-
-diff --git a/dirsrvtests/tests/suites/logging/log_flush_rotation_test.py b/dirsrvtests/tests/suites/logging/log_flush_rotation_test.py
-index b33a622e1..864ba9c5d 100644
---- a/dirsrvtests/tests/suites/logging/log_flush_rotation_test.py
-+++ b/dirsrvtests/tests/suites/logging/log_flush_rotation_test.py
-@@ -6,6 +6,7 @@
- # See LICENSE for details.
- # --- END COPYRIGHT BLOCK ---
- #
-+import glob
- import os
- import logging
- import time
-@@ -13,14 +14,351 @@ import pytest
- from lib389._constants import DEFAULT_SUFFIX, PW_DM
- from lib389.tasks import ImportTask
- from lib389.idm.user import UserAccounts
-+from lib389.idm.domain import Domain
-+from lib389.idm.directorymanager import DirectoryManager
- from lib389.topologies import topology_st as topo
-
-
- log = logging.getLogger(__name__)
-
-
-+def remove_rotated_access_logs(inst):
-+ """
-+ Remove all rotated access log files to start fresh for each test.
-+ This prevents log files from previous tests affecting current test results.
-+ """
-+ log_dir = inst.get_log_dir()
-+ patterns = [
-+ f'{log_dir}/access.2*', # Uncompressed rotated logs
-+ f'{log_dir}/access.*.gz', # Compressed rotated logs
-+ ]
-+ for pattern in patterns:
-+ for log_file in glob.glob(pattern):
-+ try:
-+ os.remove(log_file)
-+ log.info(f"Removed old log file: {log_file}")
-+ except OSError as e:
-+ log.warning(f"Could not remove {log_file}: {e}")
-+
-+
-+def reset_access_log_config(inst):
-+ """
-+ Reset access log configuration to default values.
-+ """
-+ inst.config.set('nsslapd-accesslog-compress', 'off')
-+ inst.config.set('nsslapd-accesslog-maxlogsize', '100')
-+ inst.config.set('nsslapd-accesslog-maxlogsperdir', '10')
-+ inst.config.set('nsslapd-accesslog-logrotationsync-enabled', 'off')
-+ inst.config.set('nsslapd-accesslog-logbuffering', 'on')
-+ inst.config.set('nsslapd-accesslog-logexpirationtime', '-1')
-+ inst.config.set('nsslapd-accesslog-logminfreediskspace', '5')
-+
-+
-+def generate_heavy_load(inst, suffix, iterations=50):
-+ """
-+ Generate heavy LDAP load to fill access log quickly.
-+ Performs multiple operations: searches, modifies, binds to populate logs.
-+ """
-+ for i in range(iterations):
-+ suffix.replace('description', f'iteration_{i}')
-+ suffix.get_attr_val('description')
-+
-+
-+def count_access_logs(log_dir, compressed_only=False):
-+ """
-+ Count access log files in the log directory.
-+ Returns count of rotated access logs (not including the active 'access' file).
-+ """
-+ if compressed_only:
-+ pattern = f'{log_dir}/access.*.gz'
-+ else:
-+ pattern = f'{log_dir}/access.2*'
-+ log_files = glob.glob(pattern)
-+ return len(log_files)
-+
-+
-+def test_log_pileup_with_compression(topo):
-+ """Test that log rotation properly deletes old logs when compression is enabled.
-+
-+ :id: fa1bfce8-b6d3-4520-a0a8-bead14fa5838
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Clean up existing rotated logs and reset configuration
-+ 2. Enable access log compression
-+ 3. Set strict log limits (small maxlogsperdir)
-+ 4. Disable log expiration to test count-based deletion
-+ 5. Generate heavy load to create many log rotations
-+ 6. Verify log count does not exceed maxlogsperdir limit
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ 4. Success
-+ 5. Success
-+ 6. Log count should be at or below maxlogsperdir + small buffer
-+ """
-+
-+ inst = topo.standalone
-+ suffix = Domain(inst, DEFAULT_SUFFIX)
-+ log_dir = inst.get_log_dir()
-+
-+ # Clean up before test
-+ remove_rotated_access_logs(inst)
-+ reset_access_log_config(inst)
-+ inst.restart()
-+
-+ max_logs = 5
-+ inst.config.set('nsslapd-accesslog-compress', 'on')
-+ inst.config.set('nsslapd-accesslog-maxlogsperdir', str(max_logs))
-+ inst.config.set('nsslapd-accesslog-maxlogsize', '1') # 1MB to trigger rotation
-+ inst.config.set('nsslapd-accesslog-logrotationsync-enabled', 'off')
-+ inst.config.set('nsslapd-accesslog-logbuffering', 'off')
-+
-+ inst.config.set('nsslapd-accesslog-logexpirationtime', '-1')
-+
-+ inst.config.set('nsslapd-accesslog-logminfreediskspace', '5')
-+
-+ inst.restart()
-+ time.sleep(2)
-+
-+ target_logs = max_logs * 3
-+ for i in range(target_logs):
-+ log.info(f"Generating load for log rotation {i+1}/{target_logs}")
-+ generate_heavy_load(inst, suffix, iterations=150)
-+ time.sleep(1) # Wait for rotation
-+
-+ time.sleep(3)
-+
-+ logs_on_disk = count_access_logs(log_dir)
-+ log.info(f"Configured maxlogsperdir: {max_logs}")
-+ log.info(f"Actual rotated logs on disk: {logs_on_disk}")
-+
-+ all_access_logs = glob.glob(f'{log_dir}/access*')
-+ log.info(f"All access log files: {all_access_logs}")
-+
-+ max_allowed = max_logs + 2
-+ assert logs_on_disk <= max_allowed, (
-+ f"Log rotation failed to delete old files! "
-+ f"Expected at most {max_allowed} rotated logs (maxlogsperdir={max_logs} + 2 buffer), "
-+ f"but found {logs_on_disk}. The server has lost track of the file list."
-+ )
-+
-+
-+@pytest.mark.parametrize("compress_enabled", ["on", "off"])
-+def test_accesslog_list_mismatch(topo, compress_enabled):
-+ """Test that nsslapd-accesslog-list stays synchronized with actual log files.
-+
-+ :id: 0a8a46a6-cae7-43bd-8b64-5e3481480cd3
-+ :parametrized: yes
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Clean up existing rotated logs and reset configuration
-+ 2. Configure log rotation with compression enabled/disabled
-+ 3. Generate activity to trigger multiple rotations
-+ 4. Get the nsslapd-accesslog-list attribute
-+ 5. Compare with actual files on disk
-+ 6. Verify they match (accounting for .gz extension when enabled)
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ 4. Success
-+ 5. Success
-+ 6. The list attribute should match actual files on disk
-+ """
-+
-+ inst = topo.standalone
-+ suffix = Domain(inst, DEFAULT_SUFFIX)
-+ log_dir = inst.get_log_dir()
-+ compression_on = compress_enabled == "on"
-+
-+ # Clean up before test
-+ remove_rotated_access_logs(inst)
-+ reset_access_log_config(inst)
-+ inst.restart()
-+
-+ inst.config.set('nsslapd-accesslog-compress', compress_enabled)
-+ inst.config.set('nsslapd-accesslog-maxlogsize', '1')
-+ inst.config.set('nsslapd-accesslog-maxlogsperdir', '10')
-+ inst.config.set('nsslapd-accesslog-logrotationsync-enabled', 'off')
-+ inst.config.set('nsslapd-accesslog-logbuffering', 'off')
-+ inst.config.set('nsslapd-accesslog-logexpirationtime', '-1')
-+
-+ inst.restart()
-+ time.sleep(2)
-+
-+ for i in range(15):
-+ suffix_note = "(no compression)" if not compression_on else ""
-+ log.info(f"Generating load for rotation {i+1}/15 {suffix_note}")
-+ generate_heavy_load(inst, suffix, iterations=150)
-+ time.sleep(1)
-+
-+ time.sleep(3)
-+
-+ accesslog_list = inst.config.get_attr_vals_utf8('nsslapd-accesslog-list')
-+ log.info(f"nsslapd-accesslog-list entries (compress={compress_enabled}): {len(accesslog_list)}")
-+ log.info(f"nsslapd-accesslog-list (compress={compress_enabled}): {accesslog_list}")
-+
-+ disk_files = glob.glob(f'{log_dir}/access.2*')
-+ log.info(f"Actual files on disk (compress={compress_enabled}): {len(disk_files)}")
-+ log.info(f"Disk files (compress={compress_enabled}): {disk_files}")
-+
-+ disk_files_for_compare = set()
-+ for fpath in disk_files:
-+ if compression_on and fpath.endswith('.gz'):
-+ disk_files_for_compare.add(fpath[:-3])
-+ else:
-+ disk_files_for_compare.add(fpath)
-+
-+ list_files_set = set(accesslog_list)
-+ missing_from_disk = list_files_set - disk_files_for_compare
-+ extra_on_disk = disk_files_for_compare - list_files_set
-+
-+ if missing_from_disk:
-+ log.error(
-+ f"[compress={compress_enabled}] Files in list but NOT on disk: {missing_from_disk}"
-+ )
-+ if extra_on_disk:
-+ log.warning(
-+ f"[compress={compress_enabled}] Files on disk but NOT in list: {extra_on_disk}"
-+ )
-+
-+ assert not missing_from_disk, (
-+ f"nsslapd-accesslog-list mismatch (compress={compress_enabled})! "
-+ f"Files listed but missing from disk: {missing_from_disk}. "
-+ f"This indicates the server's internal list is out of sync with actual files."
-+ )
-+
-+ if len(extra_on_disk) > 2:
-+ log.warning(
-+ f"Potential log tracking issue (compress={compress_enabled}): "
-+ f"{len(extra_on_disk)} files on disk are not tracked in the accesslog-list: "
-+ f"{extra_on_disk}"
-+ )
-+
-+
-+def test_accesslog_list_mixed_compression(topo):
-+ """Test that nsslapd-accesslog-list correctly tracks both compressed and uncompressed logs.
-+
-+ :id: 11b088cd-23be-407d-ad16-4ce2e12da09e
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Clean up existing rotated logs and reset configuration
-+ 2. Create rotated logs with compression OFF
-+ 3. Enable compression and create more rotated logs
-+ 4. Get the nsslapd-accesslog-list attribute
-+ 5. Compare with actual files on disk
-+ 6. Verify all files are correctly tracked (uncompressed and compressed)
-+ :expectedresults:
-+ 1. Success
-+ 2. Success - uncompressed rotated logs created
-+ 3. Success - compressed rotated logs created
-+ 4. Success
-+ 5. Success
-+ 6. The list should contain base filenames (without .gz) that
-+ correspond to files on disk (either as-is or with .gz suffix)
-+ """
-+
-+ inst = topo.standalone
-+ suffix = Domain(inst, DEFAULT_SUFFIX)
-+ log_dir = inst.get_log_dir()
-+
-+ # Clean up before test
-+ remove_rotated_access_logs(inst)
-+ reset_access_log_config(inst)
-+ inst.restart()
-+
-+ inst.config.set('nsslapd-accesslog-compress', 'off')
-+ inst.config.set('nsslapd-accesslog-maxlogsize', '1')
-+ inst.config.set('nsslapd-accesslog-maxlogsperdir', '20')
-+ inst.config.set('nsslapd-accesslog-logrotationsync-enabled', 'off')
-+ inst.config.set('nsslapd-accesslog-logbuffering', 'off')
-+ inst.config.set('nsslapd-accesslog-logexpirationtime', '-1')
-+
-+ inst.restart()
-+ time.sleep(2)
-+
-+ for i in range(15):
-+ log.info(f"Generating load for uncompressed rotation {i+1}/15")
-+ generate_heavy_load(inst, suffix, iterations=150)
-+ time.sleep(1)
-+
-+ time.sleep(2)
-+
-+ # Check what we have so far
-+ uncompressed_files = glob.glob(f'{log_dir}/access.2*')
-+ log.info(f"Files on disk after uncompressed phase: {uncompressed_files}")
-+
-+ inst.config.set('nsslapd-accesslog-compress', 'on')
-+ inst.restart()
-+ time.sleep(2)
-+
-+ for i in range(15):
-+ log.info(f"Generating load for compressed rotation {i+1}/15")
-+ generate_heavy_load(inst, suffix, iterations=150)
-+ time.sleep(1)
-+
-+ time.sleep(3)
-+
-+ accesslog_list = inst.config.get_attr_vals_utf8('nsslapd-accesslog-list')
-+
-+ disk_files = glob.glob(f'{log_dir}/access.2*')
-+
-+ log.info(f"nsslapd-accesslog-list entries: {len(accesslog_list)}")
-+ log.info(f"nsslapd-accesslog-list: {sorted(accesslog_list)}")
-+ log.info(f"Actual files on disk: {len(disk_files)}")
-+ log.info(f"Disk files: {sorted(disk_files)}")
-+
-+ compressed_on_disk = [f for f in disk_files if f.endswith('.gz')]
-+ uncompressed_on_disk = [f for f in disk_files if not f.endswith('.gz')]
-+ log.info(f"Compressed files on disk: {compressed_on_disk}")
-+ log.info(f"Uncompressed files on disk: {uncompressed_on_disk}")
-+
-+ list_files_set = set(accesslog_list)
-+
-+ disk_files_base = set()
-+ for fpath in disk_files:
-+ if fpath.endswith('.gz'):
-+ disk_files_base.add(fpath[:-3]) # Strip .gz
-+ else:
-+ disk_files_base.add(fpath)
-+
-+ missing_from_disk = list_files_set - disk_files_base
-+
-+ extra_on_disk = disk_files_base - list_files_set
-+
-+ if missing_from_disk:
-+ log.error(f"Files in list but NOT on disk: {missing_from_disk}")
-+ if extra_on_disk:
-+ log.warning(f"Files on disk but NOT in list: {extra_on_disk}")
-+
-+ assert not missing_from_disk, (
-+ f"nsslapd-accesslog-list contains stale entries! "
-+ f"Files in list but not on disk (as base or .gz): {missing_from_disk}"
-+ )
-+
-+ for list_file in accesslog_list:
-+ exists_uncompressed = os.path.exists(list_file)
-+ exists_compressed = os.path.exists(list_file + '.gz')
-+ assert exists_uncompressed or exists_compressed, (
-+ f"File in accesslog-list does not exist on disk: {list_file} "
-+ f"(checked both {list_file} and {list_file}.gz)"
-+ )
-+ if exists_compressed and not exists_uncompressed:
-+ log.info(f" {list_file} -> exists as .gz (compressed)")
-+ elif exists_uncompressed:
-+ log.info(f" {list_file} -> exists (uncompressed)")
-+
-+ if len(extra_on_disk) > 1:
-+ log.warning(
-+ f"Some files on disk are not tracked in accesslog-list: {extra_on_disk}"
-+ )
-+
-+ log.info("Mixed compression test completed successfully")
-+
-+
- def test_log_flush_and_rotation_crash(topo):
-- """Make sure server does not crash whening flushing a buffer and rotating
-+ """Make sure server does not crash when flushing a buffer and rotating
- the log at the same time
-
- :id: d4b0af2f-48b2-45f5-ae8b-f06f692c3133
-@@ -36,6 +374,7 @@ def test_log_flush_and_rotation_crash(topo):
- 3. Success
- 4. Success
- """
-+ # NOTE: This test is placed last as it may affect the suffix state.
-
- inst = topo.standalone
-
-diff --git a/ldap/servers/slapd/log.c b/ldap/servers/slapd/log.c
-index 27bb4bc15..ea744ac1e 100644
---- a/ldap/servers/slapd/log.c
-+++ b/ldap/servers/slapd/log.c
-@@ -137,6 +137,7 @@ static void vslapd_log_emergency_error(LOGFD fp, const char *msg, int locked);
- static int get_syslog_loglevel(int loglevel);
- static void log_external_libs_debug_openldap_print(char *buffer);
- static int log__fix_rotationinfof(char *pathname);
-+static int log__validate_rotated_logname(const char *timestamp_str, PRBool *is_compressed);
-
- static int
- get_syslog_loglevel(int loglevel)
-@@ -375,7 +376,7 @@ g_log_init()
- loginfo.log_security_fdes = NULL;
- loginfo.log_security_file = NULL;
- loginfo.log_securityinfo_file = NULL;
-- loginfo.log_numof_access_logs = 1;
-+ loginfo.log_numof_security_logs = 1;
- loginfo.log_security_logchain = NULL;
- loginfo.log_security_buffer = log_create_buffer(LOG_BUFFER_MAXSIZE);
- loginfo.log_security_compress = cfg->securitylog_compress;
-@@ -3422,7 +3423,7 @@ log__open_accesslogfile(int logfile_state, int locked)
- }
- } else if (loginfo.log_access_compress) {
- if (compress_log_file(newfile, loginfo.log_access_mode) != 0) {
-- slapi_log_err(SLAPI_LOG_ERR, "log__open_auditfaillogfile",
-+ slapi_log_err(SLAPI_LOG_ERR, "log__open_accesslogfile",
- "failed to compress rotated access log (%s)\n",
- newfile);
- } else {
-@@ -4825,6 +4826,50 @@ log__delete_rotated_logs()
- loginfo.log_error_logchain = NULL;
- }
-
-+/*
-+ * log__validate_rotated_logname
-+ *
-+ * Validates that a log filename timestamp suffix matches the expected format:
-+ * YYYYMMDD-HHMMSS (15 chars) or YYYYMMDD-HHMMSS.gz (18 chars) for compressed files.
-+ * Uses regex pattern: ^[0-9]{8}-[0-9]{6}(\.gz)?$
-+ *
-+ * \param timestamp_str The timestamp portion of the log filename (after the first '.')
-+ * \param is_compressed Output parameter set to PR_TRUE if the file has .gz suffix
-+ * \return 1 if valid, 0 if invalid
-+ */
-+static int
-+log__validate_rotated_logname(const char *timestamp_str, PRBool *is_compressed)
-+{
-+ Slapi_Regex *re = NULL;
-+ char *re_error = NULL;
-+ int rc = 0;
-+
-+ /* Match YYYYMMDD-HHMMSS with optional .gz suffix */
-+ static const char *pattern = "^[0-9]{8}-[0-9]{6}(\\.gz)?$";
-+
-+ *is_compressed = PR_FALSE;
-+
-+ re = slapi_re_comp(pattern, &re_error);
-+ if (re == NULL) {
-+ slapi_log_err(SLAPI_LOG_ERR, "log__validate_rotated_logname",
-+ "Failed to compile regex: %s\n", re_error ? re_error : "unknown error");
-+ slapi_ch_free_string(&re_error);
-+ return 0;
-+ }
-+
-+ rc = slapi_re_exec_nt(re, timestamp_str);
-+ if (rc == 1) {
-+ /* Check if compressed by looking for .gz suffix */
-+ size_t len = strlen(timestamp_str);
-+ if (len >= 3 && strcmp(timestamp_str + len - 3, ".gz") == 0) {
-+ *is_compressed = PR_TRUE;
-+ }
-+ }
-+
-+ slapi_re_free(re);
-+ return rc == 1 ? 1 : 0;
-+}
-+
- #define ERRORSLOG 1
- #define ACCESSLOG 2
- #define AUDITLOG 3
-@@ -4907,31 +4952,19 @@ log__fix_rotationinfof(char *pathname)
- }
- } else if (0 == strncmp(log_type, dirent->name, strlen(log_type)) &&
- (p = strchr(dirent->name, '.')) != NULL &&
-- NULL != strchr(p, '-')) /* e.g., errors.20051123-165135 */
-+ NULL != strchr(p, '-')) /* e.g., errors.20051123-165135 or errors.20051123-165135.gz */
- {
- struct logfileinfo *logp;
-- char *q;
-- int ignoreit = 0;
--
-- for (q = ++p; q && *q; q++) {
-- if (*q != '-' &&
-- *q != '.' && /* .gz */
-- *q != 'g' &&
-- *q != 'z' &&
-- !isdigit(*q))
-- {
-- ignoreit = 1;
-- }
-- }
-- if (ignoreit || (q - p != 15)) {
-+ PRBool is_compressed = PR_FALSE;
-+
-+ /* Skip the '.' to get the timestamp portion */
-+ p++;
-+ if (!log__validate_rotated_logname(p, &is_compressed)) {
- continue;
- }
- logp = (struct logfileinfo *)slapi_ch_malloc(sizeof(struct logfileinfo));
- logp->l_ctime = log_reverse_convert_time(p);
-- logp->l_compressed = PR_FALSE;
-- if (strcmp(p + strlen(p) - 3, ".gz") == 0) {
-- logp->l_compressed = PR_TRUE;
-- }
-+ logp->l_compressed = is_compressed;
- PR_snprintf(rotated_log, rotated_log_len, "%s/%s",
- logsdir, dirent->name);
-
-@@ -5098,23 +5131,15 @@ log__check_prevlogs(FILE *fp, char *pathname)
- for (dirent = PR_ReadDir(dirptr, dirflags); dirent;
- dirent = PR_ReadDir(dirptr, dirflags)) {
- if (0 == strncmp(log_type, dirent->name, strlen(log_type)) &&
-- (p = strrchr(dirent->name, '.')) != NULL &&
-- NULL != strchr(p, '-')) { /* e.g., errors.20051123-165135 */
-- char *q;
-- int ignoreit = 0;
--
-- for (q = ++p; q && *q; q++) {
-- if (*q != '-' &&
-- *q != '.' && /* .gz */
-- *q != 'g' &&
-- *q != 'z' &&
-- !isdigit(*q))
-- {
-- ignoreit = 1;
-- }
-- }
-- if (ignoreit || (q - p != 15))
-+ (p = strchr(dirent->name, '.')) != NULL &&
-+ NULL != strchr(p, '-')) { /* e.g., errors.20051123-165135 or errors.20051123-165135.gz */
-+ PRBool is_compressed = PR_FALSE;
-+
-+ /* Skip the '.' to get the timestamp portion */
-+ p++;
-+ if (!log__validate_rotated_logname(p, &is_compressed)) {
- continue;
-+ }
-
- fseek(fp, 0, SEEK_SET);
- buf[BUFSIZ - 1] = '\0';
---
-2.52.0
-
diff --git a/0002-Sync-lib389-version-to-3.1.4-7161.patch b/0002-Sync-lib389-version-to-3.1.4-7161.patch
deleted file mode 100644
index 08d7c94..0000000
--- a/0002-Sync-lib389-version-to-3.1.4-7161.patch
+++ /dev/null
@@ -1,37 +0,0 @@
-From 3b133a5ed6fa89939a569fe6130b325726f4e50c Mon Sep 17 00:00:00 2001
-From: Stanislav Levin <slev@altlinux.org>
-Date: Fri, 19 Dec 2025 14:52:48 +0300
-Subject: [PATCH] Sync lib389 version to 3.1.4 (#7161)
-
-Prepared with:
-$ python3 validate_version.py --update
-ERROR: Version mismatch detected!
-Main project version: 3.1.4
-lib389 version: 3.1.3
-SUCCESS: Updated lib389 version to 3.1.4 in pyproject.toml
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7160
-
-Reviewed by: @progier (Thanks!)
-
-Signed-off-by: Stanislav Levin <slev@altlinux.org>
----
- src/lib389/pyproject.toml | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/src/lib389/pyproject.toml b/src/lib389/pyproject.toml
-index 1cd840713..85c0c5141 100644
---- a/src/lib389/pyproject.toml
-+++ b/src/lib389/pyproject.toml
-@@ -16,7 +16,7 @@ build-backend = "setuptools.build_meta"
-
- [project]
- name = "lib389"
--version = "3.1.3" # Should match the main 389-ds-base version
-+version = "3.1.4" # Should match the main 389-ds-base version
- description = "A library for accessing, testing, and configuring the 389 Directory Server"
- readme = "README.md"
- license = {text = "GPL-3.0-or-later"}
---
-2.52.0
-
diff --git a/0003-Issue-7166-db_config_set-asserts-because-of-dynamic-.patch b/0003-Issue-7166-db_config_set-asserts-because-of-dynamic-.patch
deleted file mode 100644
index cf571ba..0000000
--- a/0003-Issue-7166-db_config_set-asserts-because-of-dynamic-.patch
+++ /dev/null
@@ -1,33 +0,0 @@
-From 9adaeba848a5b0fbe5d3a6148736f6c2ae940c35 Mon Sep 17 00:00:00 2001
-From: progier389 <progier@redhat.com>
-Date: Mon, 5 Jan 2026 14:38:38 +0100
-Subject: [PATCH] Issue 7166 - db_config_set asserts because of dynamic list
- (#7167)
-
-Avoid assertion in db_config_set when args does not contains dynamic list attributes
-
-Issue: #7166
-
-Reviewed by: @tbordaz (Thanks!)
-
-(cherry picked from commit 5f15223280002803a932187c22b10beaeaa74bc2)
----
- src/lib389/lib389/cli_conf/backend.py | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/src/lib389/lib389/cli_conf/backend.py b/src/lib389/lib389/cli_conf/backend.py
-index 677b37fcb..d0ec4bd9e 100644
---- a/src/lib389/lib389/cli_conf/backend.py
-+++ b/src/lib389/lib389/cli_conf/backend.py
-@@ -544,7 +544,7 @@ def db_config_set(inst, basedn, log, args):
- did_something = False
- replace_list = []
-
-- if args.enable_dynamic_lists and args.disable_dynamic_lists:
-+ if getattr(args,'enable_dynamic_lists', None) and getattr(args, 'disable_dynamic_lists', None):
- raise ValueError("You can not enable and disable dynamic lists at the same time")
-
- for attr, value in list(attrs.items()):
---
-2.52.0
-
diff --git a/0004-Issue-7160-Add-lib389-version-sync-check-to-configur.patch b/0004-Issue-7160-Add-lib389-version-sync-check-to-configur.patch
deleted file mode 100644
index 2367d6a..0000000
--- a/0004-Issue-7160-Add-lib389-version-sync-check-to-configur.patch
+++ /dev/null
@@ -1,57 +0,0 @@
-From 7693d5335c498de1dbb783042cc4acec0138e44d Mon Sep 17 00:00:00 2001
-From: Simon Pichugin <spichugi@redhat.com>
-Date: Mon, 5 Jan 2026 18:32:52 -0800
-Subject: [PATCH] Issue 7160 - Add lib389 version sync check to configure
- (#7165)
-
-Description: Add version validation during configure that ensures
-lib389 version in pyproject.toml matches the main project version
-in VERSION.sh. Configure fails with clear error message and fix
-instructions when versions mismatch, preventing inconsistent releases.
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7160
-
-Reviewed by: @progier389 (Thanks!)
----
- configure.ac | 25 +++++++++++++++++++++++++
- 1 file changed, 25 insertions(+)
-
-diff --git a/configure.ac b/configure.ac
-index e94f72647..7fd061a8c 100644
---- a/configure.ac
-+++ b/configure.ac
-@@ -7,6 +7,31 @@ AC_CONFIG_HEADERS([config.h])
- # include the version information
- . $srcdir/VERSION.sh
- AC_MSG_NOTICE(This is configure for $PACKAGE_TARNAME $PACKAGE_VERSION)
-+
-+# Validate lib389 version matches main project version
-+AC_MSG_CHECKING([lib389 version sync])
-+lib389_pyproject="$srcdir/src/lib389/pyproject.toml"
-+if test -f "$lib389_pyproject"; then
-+ lib389_version=$(grep -E '^version\s*=' "$lib389_pyproject" | sed 's/.*"\(.*\)".*/\1/')
-+ if test "x$lib389_version" != "x$RPM_VERSION"; then
-+ AC_MSG_RESULT([MISMATCH])
-+ AC_MSG_ERROR([
-+lib389 version mismatch detected!
-+ Main project version (VERSION.sh): $RPM_VERSION
-+ lib389 version (pyproject.toml): $lib389_version
-+
-+To fix this, run:
-+ cd $srcdir/src/lib389 && python3 validate_version.py --update
-+
-+lib389 version MUST match the main project version before release.
-+])
-+ else
-+ AC_MSG_RESULT([ok ($lib389_version)])
-+ fi
-+else
-+ AC_MSG_RESULT([MISSING])
-+ AC_MSG_ERROR([lib389 pyproject.toml not found at $lib389_pyproject - source tree is incomplete])
-+fi
- AM_INIT_AUTOMAKE([1.9 foreign subdir-objects dist-bzip2 no-dist-gzip no-define tar-pax])
- AC_SUBST([RPM_VERSION])
- AC_SUBST([RPM_RELEASE])
---
-2.52.0
-
diff --git a/0005-Issue-7096-During-replication-online-total-init-the-.patch b/0005-Issue-7096-During-replication-online-total-init-the-.patch
deleted file mode 100644
index fddca98..0000000
--- a/0005-Issue-7096-During-replication-online-total-init-the-.patch
+++ /dev/null
@@ -1,318 +0,0 @@
-From 5de98cdc10bd333d0695c00d57e137d699f90f1c Mon Sep 17 00:00:00 2001
-From: tbordaz <tbordaz@redhat.com>
-Date: Wed, 7 Jan 2026 11:21:12 +0100
-Subject: [PATCH] Issue 7096 - During replication online total init the
- function idl_id_is_in_idlist is not scaling with large database (#7145)
-
-Bug description:
- During a online total initialization, the supplier sorts
- the candidate list of entries so that the parents are sent before
- children entries.
- With large DB the ID array used for the sorting is not
- scaling. It takes so long to build the candidate list that
- the connection gets closed
-
-Fix description:
- Instead of using an ID array, uses a list of ID ranges
-
-fixes: #7096
-
-Reviewed by: Mark Reynolds, Pierre Rogier (Thanks !!)
----
- ldap/servers/slapd/back-ldbm/back-ldbm.h | 12 ++
- ldap/servers/slapd/back-ldbm/idl_common.c | 163 ++++++++++++++++++
- ldap/servers/slapd/back-ldbm/idl_new.c | 30 ++--
- .../servers/slapd/back-ldbm/proto-back-ldbm.h | 3 +
- 4 files changed, 189 insertions(+), 19 deletions(-)
-
-diff --git a/ldap/servers/slapd/back-ldbm/back-ldbm.h b/ldap/servers/slapd/back-ldbm/back-ldbm.h
-index 1bc36720d..b187c26bc 100644
---- a/ldap/servers/slapd/back-ldbm/back-ldbm.h
-+++ b/ldap/servers/slapd/back-ldbm/back-ldbm.h
-@@ -282,6 +282,18 @@ typedef struct _idlist_set
- #define INDIRECT_BLOCK(idl) ((idl)->b_nids == INDBLOCK)
- #define IDL_NIDS(idl) (idl ? (idl)->b_nids : (NIDS)0)
-
-+/*
-+ * used by the supplier during online total init
-+ * it stores the ranges of ID that are already present
-+ * in the candidate list ('parentid>=1')
-+ */
-+typedef struct IdRange {
-+ ID first;
-+ ID last;
-+ struct IdRange *next;
-+} IdRange_t;
-+
-+
- typedef size_t idl_iterator;
-
- /* small hashtable implementation used in the entry cache -- the table
-diff --git a/ldap/servers/slapd/back-ldbm/idl_common.c b/ldap/servers/slapd/back-ldbm/idl_common.c
-index fcb0ece4b..fdc9b4e67 100644
---- a/ldap/servers/slapd/back-ldbm/idl_common.c
-+++ b/ldap/servers/slapd/back-ldbm/idl_common.c
-@@ -172,6 +172,169 @@ idl_min(IDList *a, IDList *b)
- return (a->b_nids > b->b_nids ? b : a);
- }
-
-+/*
-+ * This is a faster version of idl_id_is_in_idlist.
-+ * idl_id_is_in_idlist uses an array of ID so lookup is expensive
-+ * idl_id_is_in_idlist_ranges uses a list of ranges of ID lookup is faster
-+ * returns
-+ * 1: 'id' is present in idrange_list
-+ * 0: 'id' is not present in idrange_list
-+ */
-+int
-+idl_id_is_in_idlist_ranges(IDList *idl, IdRange_t *idrange_list, ID id)
-+{
-+ IdRange_t *range = idrange_list;
-+ int found = 0;
-+
-+ if (NULL == idl || NOID == id) {
-+ return 0; /* not in the list */
-+ }
-+ if (ALLIDS(idl)) {
-+ return 1; /* in the list */
-+ }
-+
-+ for(;range; range = range->next) {
-+ if (id > range->last) {
-+ /* check if it belongs to the next range */
-+ continue;
-+ }
-+ if (id >= range->first) {
-+ /* It belongs to that range [first..last ] */
-+ found = 1;
-+ break;
-+ } else {
-+ /* this range is after id */
-+ break;
-+ }
-+ }
-+ return found;
-+}
-+
-+/* This function is used during the online total initialisation
-+ * (see next function)
-+ * It frees all ranges of ID in the list
-+ */
-+void idrange_free(IdRange_t **head)
-+{
-+ IdRange_t *curr, *sav;
-+
-+ if ((head == NULL) || (*head == NULL)) {
-+ return;
-+ }
-+ curr = *head;
-+ sav = NULL;
-+ for (; curr;) {
-+ sav = curr;
-+ curr = curr->next;
-+ slapi_ch_free((void *) &sav);
-+ }
-+ if (sav) {
-+ slapi_ch_free((void *) &sav);
-+ }
-+ *head = NULL;
-+}
-+
-+/* This function is used during the online total initialisation
-+ * Because a MODRDN can move entries under a parent that
-+ * has a higher ID we need to sort the IDList so that parents
-+ * are sent, to the consumer, before the children are sent.
-+ * The sorting with a simple IDlist does not scale instead
-+ * a list of IDs ranges is much faster.
-+ * In that list we only ADD/lookup ID.
-+ */
-+IdRange_t *idrange_add_id(IdRange_t **head, ID id)
-+{
-+ if (head == NULL) {
-+ slapi_log_err(SLAPI_LOG_ERR, "idrange_add_id",
-+ "Can not add ID %d in non defined list\n", id);
-+ return NULL;
-+ }
-+
-+ if (*head == NULL) {
-+ /* This is the first range */
-+ IdRange_t *new_range = (IdRange_t *)slapi_ch_malloc(sizeof(IdRange_t));
-+ new_range->first = id;
-+ new_range->last = id;
-+ new_range->next = NULL;
-+ *head = new_range;
-+ return *head;
-+ }
-+
-+ IdRange_t *curr = *head, *prev = NULL;
-+
-+ /* First, find if id already falls within any existing range, or it is adjacent to any */
-+ while (curr) {
-+ if (id >= curr->first && id <= curr->last) {
-+ /* inside a range, nothing to do */
-+ return curr;
-+ }
-+
-+ if (id == curr->last + 1) {
-+ /* Extend this range upwards */
-+ curr->last = id;
-+
-+ /* Check for possible merge with next range */
-+ IdRange_t *next = curr->next;
-+ if (next && curr->last + 1 >= next->first) {
-+ slapi_log_err(SLAPI_LOG_REPL, "idrange_add_id",
-+ "(id=%d) merge current with next range [%d..%d]\n", id, curr->first, curr->last);
-+ curr->last = (next->last > curr->last) ? next->last : curr->last;
-+ curr->next = next->next;
-+ slapi_ch_free((void*) &next);
-+ } else {
-+ slapi_log_err(SLAPI_LOG_REPL, "idrange_add_id",
-+ "(id=%d) extend forward current range [%d..%d]\n", id, curr->first, curr->last);
-+ }
-+ return curr;
-+ }
-+
-+ if (id + 1 == curr->first) {
-+ /* Extend this range downwards */
-+ curr->first = id;
-+
-+ /* Check for possible merge with previous range */
-+ if (prev && prev->last + 1 >= curr->first) {
-+ prev->last = curr->last;
-+ prev->next = curr->next;
-+ slapi_ch_free((void *) &curr);
-+ slapi_log_err(SLAPI_LOG_REPL, "idrange_add_id",
-+ "(id=%d) merge current with previous range [%d..%d]\n", id, prev->first, prev->last);
-+ return prev;
-+ } else {
-+ slapi_log_err(SLAPI_LOG_REPL, "idrange_add_id",
-+ "(id=%d) extend backward current range [%d..%d]\n", id, curr->first, curr->last);
-+ return curr;
-+ }
-+ }
-+
-+ /* If id is before the current range, break so we can insert before */
-+ if (id < curr->first) {
-+ break;
-+ }
-+
-+ prev = curr;
-+ curr = curr->next;
-+ }
-+ /* Need to insert a new standalone IdRange */
-+ IdRange_t *new_range = (IdRange_t *)slapi_ch_malloc(sizeof(IdRange_t));
-+ new_range->first = id;
-+ new_range->last = id;
-+ new_range->next = curr;
-+
-+ if (prev) {
-+ slapi_log_err(SLAPI_LOG_REPL, "idrange_add_id",
-+ "(id=%d) add new range [%d..%d]\n", id, new_range->first, new_range->last);
-+ prev->next = new_range;
-+ } else {
-+ /* Insert at head */
-+ slapi_log_err(SLAPI_LOG_REPL, "idrange_add_id",
-+ "(id=%d) head range [%d..%d]\n", id, new_range->first, new_range->last);
-+ *head = new_range;
-+ }
-+ return *head;
-+}
-+
-+
- int
- idl_id_is_in_idlist(IDList *idl, ID id)
- {
-diff --git a/ldap/servers/slapd/back-ldbm/idl_new.c b/ldap/servers/slapd/back-ldbm/idl_new.c
-index 5fbcaff2e..2d978353f 100644
---- a/ldap/servers/slapd/back-ldbm/idl_new.c
-+++ b/ldap/servers/slapd/back-ldbm/idl_new.c
-@@ -417,7 +417,6 @@ idl_new_range_fetch(
- {
- int ret = 0;
- int ret2 = 0;
-- int idl_rc = 0;
- dbi_cursor_t cursor = {0};
- IDList *idl = NULL;
- dbi_val_t cur_key = {0};
-@@ -436,6 +435,7 @@ idl_new_range_fetch(
- size_t leftoverlen = 32;
- size_t leftovercnt = 0;
- char *index_id = get_index_name(be, db, ai);
-+ IdRange_t *idrange_list = NULL;
-
-
- if (NULL == flag_err) {
-@@ -578,10 +578,12 @@ idl_new_range_fetch(
- * found entry is the one from the suffix
- */
- suffix = key;
-- idl_rc = idl_append_extend(&idl, id);
-- } else if ((key == suffix) || idl_id_is_in_idlist(idl, key)) {
-+ idl_append_extend(&idl, id);
-+ idrange_add_id(&idrange_list, id);
-+ } else if ((key == suffix) || idl_id_is_in_idlist_ranges(idl, idrange_list, key)) {
- /* the parent is the suffix or already in idl. */
-- idl_rc = idl_append_extend(&idl, id);
-+ idl_append_extend(&idl, id);
-+ idrange_add_id(&idrange_list, id);
- } else {
- /* Otherwise, keep the {key,id} in leftover array */
- if (!leftover) {
-@@ -596,13 +598,7 @@ idl_new_range_fetch(
- leftovercnt++;
- }
- } else {
-- idl_rc = idl_append_extend(&idl, id);
-- }
-- if (idl_rc) {
-- slapi_log_err(SLAPI_LOG_ERR, "idl_new_range_fetch",
-- "Unable to extend id list (err=%d)\n", idl_rc);
-- idl_free(&idl);
-- goto error;
-+ idl_append_extend(&idl, id);
- }
-
- count++;
-@@ -695,21 +691,17 @@ error:
-
- while(remaining > 0) {
- for (size_t i = 0; i < leftovercnt; i++) {
-- if (leftover[i].key > 0 && idl_id_is_in_idlist(idl, leftover[i].key) != 0) {
-+ if (leftover[i].key > 0 && idl_id_is_in_idlist_ranges(idl, idrange_list, leftover[i].key) != 0) {
- /* if the leftover key has its parent in the idl */
-- idl_rc = idl_append_extend(&idl, leftover[i].id);
-- if (idl_rc) {
-- slapi_log_err(SLAPI_LOG_ERR, "idl_new_range_fetch",
-- "Unable to extend id list (err=%d)\n", idl_rc);
-- idl_free(&idl);
-- return NULL;
-- }
-+ idl_append_extend(&idl, leftover[i].id);
-+ idrange_add_id(&idrange_list, leftover[i].id);
- leftover[i].key = 0;
- remaining--;
- }
- }
- }
- slapi_ch_free((void **)&leftover);
-+ idrange_free(&idrange_list);
- }
- slapi_log_err(SLAPI_LOG_FILTER, "idl_new_range_fetch",
- "Found %d candidates; error code is: %d\n",
-diff --git a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
-index 91d61098a..30a7aa11f 100644
---- a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
-+++ b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
-@@ -217,6 +217,9 @@ ID idl_firstid(IDList *idl);
- ID idl_nextid(IDList *idl, ID id);
- int idl_init_private(backend *be, struct attrinfo *a);
- int idl_release_private(struct attrinfo *a);
-+IdRange_t *idrange_add_id(IdRange_t **head, ID id);
-+void idrange_free(IdRange_t **head);
-+int idl_id_is_in_idlist_ranges(IDList *idl, IdRange_t *idrange_list, ID id);
- int idl_id_is_in_idlist(IDList *idl, ID id);
-
- idl_iterator idl_iterator_init(const IDList *idl);
---
-2.52.0
-
diff --git a/0006-Issue-Revise-paged-result-search-locking.patch b/0006-Issue-Revise-paged-result-search-locking.patch
deleted file mode 100644
index 10e7dd5..0000000
--- a/0006-Issue-Revise-paged-result-search-locking.patch
+++ /dev/null
@@ -1,765 +0,0 @@
-From 6f3bf5a48d504646751be9e91293487eec972ed8 Mon Sep 17 00:00:00 2001
-From: Mark Reynolds <mreynolds@redhat.com>
-Date: Wed, 7 Jan 2026 16:55:27 -0500
-Subject: [PATCH] Issue - Revise paged result search locking
-
-Description:
-
-Move to a single lock approach verses having two locks. This will impact
-concurrency when multiple async paged result searches are done on the same
-connection, but it simplifies the code and avoids race conditions and
-deadlocks.
-
-Relates: https://github.com/389ds/389-ds-base/issues/7118
-
-Reviewed by: progier & tbordaz (Thanks!!)
----
- ldap/servers/slapd/abandon.c | 2 +-
- ldap/servers/slapd/opshared.c | 60 ++++----
- ldap/servers/slapd/pagedresults.c | 228 +++++++++++++++++++-----------
- ldap/servers/slapd/proto-slap.h | 26 ++--
- ldap/servers/slapd/slap.h | 5 +-
- 5 files changed, 187 insertions(+), 134 deletions(-)
-
-diff --git a/ldap/servers/slapd/abandon.c b/ldap/servers/slapd/abandon.c
-index 6024fcd31..1f47c531c 100644
---- a/ldap/servers/slapd/abandon.c
-+++ b/ldap/servers/slapd/abandon.c
-@@ -179,7 +179,7 @@ do_abandon(Slapi_PBlock *pb)
- logpb.tv_sec = -1;
- logpb.tv_nsec = -1;
-
-- if (0 == pagedresults_free_one_msgid(pb_conn, id, pageresult_lock_get_addr(pb_conn))) {
-+ if (0 == pagedresults_free_one_msgid(pb_conn, id, PR_NOT_LOCKED)) {
- if (log_format != LOG_FORMAT_DEFAULT) {
- /* JSON logging */
- logpb.target_op = "Simple Paged Results";
-diff --git a/ldap/servers/slapd/opshared.c b/ldap/servers/slapd/opshared.c
-index a5cddfd23..bf800f7dc 100644
---- a/ldap/servers/slapd/opshared.c
-+++ b/ldap/servers/slapd/opshared.c
-@@ -572,8 +572,8 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
- be = be_list[index];
- }
- }
-- pr_search_result = pagedresults_get_search_result(pb_conn, operation, 0 /*not locked*/, pr_idx);
-- estimate = pagedresults_get_search_result_set_size_estimate(pb_conn, operation, pr_idx);
-+ pr_search_result = pagedresults_get_search_result(pb_conn, operation, PR_NOT_LOCKED, pr_idx);
-+ estimate = pagedresults_get_search_result_set_size_estimate(pb_conn, operation, PR_NOT_LOCKED, pr_idx);
- /* Set operation note flags as required. */
- if (pagedresults_get_unindexed(pb_conn, operation, pr_idx)) {
- slapi_pblock_set_flag_operation_notes(pb, SLAPI_OP_NOTE_UNINDEXED);
-@@ -619,14 +619,7 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
- int32_t tlimit;
- slapi_pblock_get(pb, SLAPI_SEARCH_TIMELIMIT, &tlimit);
- pagedresults_set_timelimit(pb_conn, operation, (time_t)tlimit, pr_idx);
-- /* When using this mutex in conjunction with the main paged
-- * result lock, you must do so in this order:
-- *
-- * --> pagedresults_lock()
-- * --> pagedresults_mutex
-- * <-- pagedresults_mutex
-- * <-- pagedresults_unlock()
-- */
-+ /* IMPORTANT: Never acquire pagedresults_mutex when holding c_mutex. */
- pagedresults_mutex = pageresult_lock_get_addr(pb_conn);
- }
-
-@@ -743,17 +736,15 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
- if (op_is_pagedresults(operation) && pr_search_result) {
- void *sr = NULL;
- /* PAGED RESULTS and already have the search results from the prev op */
-- pagedresults_lock(pb_conn, pr_idx);
- /*
- * In async paged result case, the search result might be released
- * by other theads. We need to double check it in the locked region.
- */
- pthread_mutex_lock(pagedresults_mutex);
-- pr_search_result = pagedresults_get_search_result(pb_conn, operation, 1 /*locked*/, pr_idx);
-+ pr_search_result = pagedresults_get_search_result(pb_conn, operation, PR_LOCKED, pr_idx);
- if (pr_search_result) {
-- if (pagedresults_is_abandoned_or_notavailable(pb_conn, 1 /*locked*/, pr_idx)) {
-+ if (pagedresults_is_abandoned_or_notavailable(pb_conn, PR_LOCKED, pr_idx)) {
- pthread_mutex_unlock(pagedresults_mutex);
-- pagedresults_unlock(pb_conn, pr_idx);
- /* Previous operation was abandoned and the simplepaged object is not in use. */
- send_ldap_result(pb, 0, NULL, "Simple Paged Results Search abandoned", 0, NULL);
- rc = LDAP_SUCCESS;
-@@ -764,14 +755,13 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
-
- /* search result could be reset in the backend/dse */
- slapi_pblock_get(pb, SLAPI_SEARCH_RESULT_SET, &sr);
-- pagedresults_set_search_result(pb_conn, operation, sr, 1 /*locked*/, pr_idx);
-+ pagedresults_set_search_result(pb_conn, operation, sr, PR_LOCKED, pr_idx);
- }
- } else {
- pr_stat = PAGEDRESULTS_SEARCH_END;
- rc = LDAP_SUCCESS;
- }
- pthread_mutex_unlock(pagedresults_mutex);
-- pagedresults_unlock(pb_conn, pr_idx);
-
- if ((PAGEDRESULTS_SEARCH_END == pr_stat) || (0 == pnentries)) {
- /* no more entries to send in the backend */
-@@ -789,22 +779,22 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
- }
- pagedresults_set_response_control(pb, 0, estimate,
- curr_search_count, pr_idx);
-- if (pagedresults_get_with_sort(pb_conn, operation, pr_idx)) {
-+ if (pagedresults_get_with_sort(pb_conn, operation, PR_NOT_LOCKED, pr_idx)) {
- sort_make_sort_response_control(pb, CONN_GET_SORT_RESULT_CODE, NULL);
- }
- pagedresults_set_search_result_set_size_estimate(pb_conn,
- operation,
-- estimate, pr_idx);
-+ estimate, PR_NOT_LOCKED, pr_idx);
- if (PAGEDRESULTS_SEARCH_END == pr_stat) {
-- pagedresults_lock(pb_conn, pr_idx);
-+ pthread_mutex_lock(pagedresults_mutex);
- slapi_pblock_set(pb, SLAPI_SEARCH_RESULT_SET, NULL);
-- if (!pagedresults_is_abandoned_or_notavailable(pb_conn, 0 /*not locked*/, pr_idx)) {
-- pagedresults_free_one(pb_conn, operation, pr_idx);
-+ if (!pagedresults_is_abandoned_or_notavailable(pb_conn, PR_LOCKED, pr_idx)) {
-+ pagedresults_free_one(pb_conn, operation, PR_LOCKED, pr_idx);
- }
-- pagedresults_unlock(pb_conn, pr_idx);
-+ pthread_mutex_unlock(pagedresults_mutex);
- if (next_be) {
- /* no more entries, but at least another backend */
-- if (pagedresults_set_current_be(pb_conn, next_be, pr_idx, 0) < 0) {
-+ if (pagedresults_set_current_be(pb_conn, next_be, pr_idx, PR_NOT_LOCKED) < 0) {
- goto free_and_return;
- }
- }
-@@ -915,7 +905,7 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
- }
- }
- pagedresults_set_search_result(pb_conn, operation, NULL, 1, pr_idx);
-- rc = pagedresults_set_current_be(pb_conn, NULL, pr_idx, 1);
-+ rc = pagedresults_set_current_be(pb_conn, NULL, pr_idx, PR_LOCKED);
- pthread_mutex_unlock(pagedresults_mutex);
- #pragma GCC diagnostic pop
- }
-@@ -954,7 +944,7 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
- pthread_mutex_lock(pagedresults_mutex);
- pagedresults_set_search_result(pb_conn, operation, NULL, 1, pr_idx);
- be->be_search_results_release(&sr);
-- rc = pagedresults_set_current_be(pb_conn, next_be, pr_idx, 1);
-+ rc = pagedresults_set_current_be(pb_conn, next_be, pr_idx, PR_LOCKED);
- pthread_mutex_unlock(pagedresults_mutex);
- pr_stat = PAGEDRESULTS_SEARCH_END; /* make sure stat is SEARCH_END */
- if (NULL == next_be) {
-@@ -967,23 +957,23 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
- } else {
- curr_search_count = pnentries;
- slapi_pblock_get(pb, SLAPI_SEARCH_RESULT_SET_SIZE_ESTIMATE, &estimate);
-- pagedresults_lock(pb_conn, pr_idx);
-- if ((pagedresults_set_current_be(pb_conn, be, pr_idx, 0) < 0) ||
-- (pagedresults_set_search_result(pb_conn, operation, sr, 0, pr_idx) < 0) ||
-- (pagedresults_set_search_result_count(pb_conn, operation, curr_search_count, pr_idx) < 0) ||
-- (pagedresults_set_search_result_set_size_estimate(pb_conn, operation, estimate, pr_idx) < 0) ||
-- (pagedresults_set_with_sort(pb_conn, operation, with_sort, pr_idx) < 0)) {
-- pagedresults_unlock(pb_conn, pr_idx);
-+ pthread_mutex_lock(pagedresults_mutex);
-+ if ((pagedresults_set_current_be(pb_conn, be, pr_idx, PR_LOCKED) < 0) ||
-+ (pagedresults_set_search_result(pb_conn, operation, sr, PR_LOCKED, pr_idx) < 0) ||
-+ (pagedresults_set_search_result_count(pb_conn, operation, curr_search_count, PR_LOCKED, pr_idx) < 0) ||
-+ (pagedresults_set_search_result_set_size_estimate(pb_conn, operation, estimate, PR_LOCKED, pr_idx) < 0) ||
-+ (pagedresults_set_with_sort(pb_conn, operation, with_sort, PR_LOCKED, pr_idx) < 0)) {
-+ pthread_mutex_unlock(pagedresults_mutex);
- cache_return_target_entry(pb, be, operation);
- goto free_and_return;
- }
-- pagedresults_unlock(pb_conn, pr_idx);
-+ pthread_mutex_unlock(pagedresults_mutex);
- }
- slapi_pblock_set(pb, SLAPI_SEARCH_RESULT_SET, NULL);
- next_be = NULL; /* to break the loop */
- if (operation->o_status & SLAPI_OP_STATUS_ABANDONED) {
- /* It turned out this search was abandoned. */
-- pagedresults_free_one_msgid(pb_conn, operation->o_msgid, pagedresults_mutex);
-+ pagedresults_free_one_msgid(pb_conn, operation->o_msgid, PR_NOT_LOCKED);
- /* paged-results-request was abandoned; making an empty cookie. */
- pagedresults_set_response_control(pb, 0, estimate, -1, pr_idx);
- send_ldap_result(pb, 0, NULL, "Simple Paged Results Search abandoned", 0, NULL);
-@@ -993,7 +983,7 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
- }
- pagedresults_set_response_control(pb, 0, estimate, curr_search_count, pr_idx);
- if (curr_search_count == -1) {
-- pagedresults_free_one(pb_conn, operation, pr_idx);
-+ pagedresults_free_one(pb_conn, operation, PR_NOT_LOCKED, pr_idx);
- }
- }
-
-diff --git a/ldap/servers/slapd/pagedresults.c b/ldap/servers/slapd/pagedresults.c
-index 941ab97e3..0d6c4a1aa 100644
---- a/ldap/servers/slapd/pagedresults.c
-+++ b/ldap/servers/slapd/pagedresults.c
-@@ -34,9 +34,9 @@ pageresult_lock_cleanup()
- slapi_ch_free((void**)&lock_hash);
- }
-
--/* Beware to the lock order with c_mutex:
-- * c_mutex is sometime locked while holding pageresult_lock
-- * ==> Do not lock pageresult_lock when holing c_mutex
-+/* Lock ordering constraint with c_mutex:
-+ * c_mutex is sometimes locked while holding pageresult_lock.
-+ * Therefore: DO NOT acquire pageresult_lock when holding c_mutex.
- */
- pthread_mutex_t *
- pageresult_lock_get_addr(Connection *conn)
-@@ -44,7 +44,11 @@ pageresult_lock_get_addr(Connection *conn)
- return &lock_hash[(((size_t)conn)/sizeof (Connection))%LOCK_HASH_SIZE];
- }
-
--/* helper function to clean up one prp slot */
-+/* helper function to clean up one prp slot
-+ *
-+ * NOTE: This function must be called while holding the pageresult_lock
-+ * (via pageresult_lock_get_addr(conn)) to ensure thread-safe cleanup.
-+ */
- static void
- _pr_cleanup_one_slot(PagedResults *prp)
- {
-@@ -56,7 +60,7 @@ _pr_cleanup_one_slot(PagedResults *prp)
- prp->pr_current_be->be_search_results_release(&(prp->pr_search_result_set));
- }
-
-- /* clean up the slot except the mutex */
-+ /* clean up the slot */
- prp->pr_current_be = NULL;
- prp->pr_search_result_set = NULL;
- prp->pr_search_result_count = 0;
-@@ -136,6 +140,8 @@ pagedresults_parse_control_value(Slapi_PBlock *pb,
- return LDAP_UNWILLING_TO_PERFORM;
- }
-
-+ /* Acquire hash-based lock for paged results list access
-+ * IMPORTANT: Never acquire this lock when holding c_mutex */
- pthread_mutex_lock(pageresult_lock_get_addr(conn));
- /* the ber encoding is no longer needed */
- ber_free(ber, 1);
-@@ -184,10 +190,6 @@ pagedresults_parse_control_value(Slapi_PBlock *pb,
- goto bail;
- }
-
-- if ((*index > -1) && (*index < conn->c_pagedresults.prl_maxlen) &&
-- !conn->c_pagedresults.prl_list[*index].pr_mutex) {
-- conn->c_pagedresults.prl_list[*index].pr_mutex = PR_NewLock();
-- }
- conn->c_pagedresults.prl_count++;
- } else {
- /* Repeated paged results request.
-@@ -327,8 +329,14 @@ bailout:
- "<= idx=%d\n", index);
- }
-
-+/*
-+ * Free one paged result entry by index.
-+ *
-+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
-+ * caller already holds pageresult_lock. Never call when holding c_mutex.
-+ */
- int
--pagedresults_free_one(Connection *conn, Operation *op, int index)
-+pagedresults_free_one(Connection *conn, Operation *op, bool locked, int index)
- {
- int rc = -1;
-
-@@ -338,7 +346,9 @@ pagedresults_free_one(Connection *conn, Operation *op, int index)
- slapi_log_err(SLAPI_LOG_TRACE, "pagedresults_free_one",
- "=> idx=%d\n", index);
- if (conn && (index > -1)) {
-- pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ }
- if (conn->c_pagedresults.prl_count <= 0) {
- slapi_log_err(SLAPI_LOG_TRACE, "pagedresults_free_one",
- "conn=%" PRIu64 " paged requests list count is %d\n",
-@@ -349,7 +359,9 @@ pagedresults_free_one(Connection *conn, Operation *op, int index)
- conn->c_pagedresults.prl_count--;
- rc = 0;
- }
-- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ }
- }
-
- slapi_log_err(SLAPI_LOG_TRACE, "pagedresults_free_one", "<= %d\n", rc);
-@@ -357,21 +369,28 @@ pagedresults_free_one(Connection *conn, Operation *op, int index)
- }
-
- /*
-- * Used for abandoning - pageresult_lock_get_addr(conn) is already locked in do_abandone.
-+ * Free one paged result entry by message ID.
-+ *
-+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
-+ * caller already holds pageresult_lock. Never call when holding c_mutex.
- */
- int
--pagedresults_free_one_msgid(Connection *conn, ber_int_t msgid, pthread_mutex_t *mutex)
-+pagedresults_free_one_msgid(Connection *conn, ber_int_t msgid, bool locked)
- {
- int rc = -1;
- int i;
-+ pthread_mutex_t *lock = NULL;
-
- if (conn && (msgid > -1)) {
- if (conn->c_pagedresults.prl_maxlen <= 0) {
- ; /* Not a paged result. */
- } else {
- slapi_log_err(SLAPI_LOG_TRACE,
-- "pagedresults_free_one_msgid_nolock", "=> msgid=%d\n", msgid);
-- pthread_mutex_lock(mutex);
-+ "pagedresults_free_one_msgid", "=> msgid=%d\n", msgid);
-+ lock = pageresult_lock_get_addr(conn);
-+ if (!locked) {
-+ pthread_mutex_lock(lock);
-+ }
- for (i = 0; i < conn->c_pagedresults.prl_maxlen; i++) {
- if (conn->c_pagedresults.prl_list[i].pr_msgid == msgid) {
- PagedResults *prp = conn->c_pagedresults.prl_list + i;
-@@ -390,9 +409,11 @@ pagedresults_free_one_msgid(Connection *conn, ber_int_t msgid, pthread_mutex_t *
- break;
- }
- }
-- pthread_mutex_unlock(mutex);
-+ if (!locked) {
-+ pthread_mutex_unlock(lock);
-+ }
- slapi_log_err(SLAPI_LOG_TRACE,
-- "pagedresults_free_one_msgid_nolock", "<= %d\n", rc);
-+ "pagedresults_free_one_msgid", "<= %d\n", rc);
- }
- }
-
-@@ -418,29 +439,43 @@ pagedresults_get_current_be(Connection *conn, int index)
- return be;
- }
-
-+/*
-+ * Set current backend for a paged result entry.
-+ *
-+ * Locking: If locked=false, acquires pageresult_lock. If locked=true, assumes
-+ * caller already holds pageresult_lock. Never call when holding c_mutex.
-+ */
- int
--pagedresults_set_current_be(Connection *conn, Slapi_Backend *be, int index, int nolock)
-+pagedresults_set_current_be(Connection *conn, Slapi_Backend *be, int index, bool locked)
- {
- int rc = -1;
- slapi_log_err(SLAPI_LOG_TRACE,
- "pagedresults_set_current_be", "=> idx=%d\n", index);
- if (conn && (index > -1)) {
-- if (!nolock)
-+ if (!locked) {
- pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ }
- if (index < conn->c_pagedresults.prl_maxlen) {
- conn->c_pagedresults.prl_list[index].pr_current_be = be;
- }
- rc = 0;
-- if (!nolock)
-+ if (!locked) {
- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ }
- }
- slapi_log_err(SLAPI_LOG_TRACE,
- "pagedresults_set_current_be", "<= %d\n", rc);
- return rc;
- }
-
-+/*
-+ * Get search result set for a paged result entry.
-+ *
-+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
-+ * caller already holds pageresult_lock. Never call when holding c_mutex.
-+ */
- void *
--pagedresults_get_search_result(Connection *conn, Operation *op, int locked, int index)
-+pagedresults_get_search_result(Connection *conn, Operation *op, bool locked, int index)
- {
- void *sr = NULL;
- if (!op_is_pagedresults(op)) {
-@@ -465,8 +500,14 @@ pagedresults_get_search_result(Connection *conn, Operation *op, int locked, int
- return sr;
- }
-
-+/*
-+ * Set search result set for a paged result entry.
-+ *
-+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
-+ * caller already holds pageresult_lock. Never call when holding c_mutex.
-+ */
- int
--pagedresults_set_search_result(Connection *conn, Operation *op, void *sr, int locked, int index)
-+pagedresults_set_search_result(Connection *conn, Operation *op, void *sr, bool locked, int index)
- {
- int rc = -1;
- if (!op_is_pagedresults(op)) {
-@@ -494,8 +535,14 @@ pagedresults_set_search_result(Connection *conn, Operation *op, void *sr, int lo
- return rc;
- }
-
-+/*
-+ * Get search result count for a paged result entry.
-+ *
-+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
-+ * caller already holds pageresult_lock. Never call when holding c_mutex.
-+ */
- int
--pagedresults_get_search_result_count(Connection *conn, Operation *op, int index)
-+pagedresults_get_search_result_count(Connection *conn, Operation *op, bool locked, int index)
- {
- int count = 0;
- if (!op_is_pagedresults(op)) {
-@@ -504,19 +551,29 @@ pagedresults_get_search_result_count(Connection *conn, Operation *op, int index)
- slapi_log_err(SLAPI_LOG_TRACE,
- "pagedresults_get_search_result_count", "=> idx=%d\n", index);
- if (conn && (index > -1)) {
-- pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ }
- if (index < conn->c_pagedresults.prl_maxlen) {
- count = conn->c_pagedresults.prl_list[index].pr_search_result_count;
- }
-- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ }
- }
- slapi_log_err(SLAPI_LOG_TRACE,
- "pagedresults_get_search_result_count", "<= %d\n", count);
- return count;
- }
-
-+/*
-+ * Set search result count for a paged result entry.
-+ *
-+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
-+ * caller already holds pageresult_lock. Never call when holding c_mutex.
-+ */
- int
--pagedresults_set_search_result_count(Connection *conn, Operation *op, int count, int index)
-+pagedresults_set_search_result_count(Connection *conn, Operation *op, int count, bool locked, int index)
- {
- int rc = -1;
- if (!op_is_pagedresults(op)) {
-@@ -525,11 +582,15 @@ pagedresults_set_search_result_count(Connection *conn, Operation *op, int count,
- slapi_log_err(SLAPI_LOG_TRACE,
- "pagedresults_set_search_result_count", "=> idx=%d\n", index);
- if (conn && (index > -1)) {
-- pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ }
- if (index < conn->c_pagedresults.prl_maxlen) {
- conn->c_pagedresults.prl_list[index].pr_search_result_count = count;
- }
-- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ }
- rc = 0;
- }
- slapi_log_err(SLAPI_LOG_TRACE,
-@@ -537,9 +598,16 @@ pagedresults_set_search_result_count(Connection *conn, Operation *op, int count,
- return rc;
- }
-
-+/*
-+ * Get search result set size estimate for a paged result entry.
-+ *
-+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
-+ * caller already holds pageresult_lock. Never call when holding c_mutex.
-+ */
- int
- pagedresults_get_search_result_set_size_estimate(Connection *conn,
- Operation *op,
-+ bool locked,
- int index)
- {
- int count = 0;
-@@ -550,11 +618,15 @@ pagedresults_get_search_result_set_size_estimate(Connection *conn,
- "pagedresults_get_search_result_set_size_estimate",
- "=> idx=%d\n", index);
- if (conn && (index > -1)) {
-- pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ }
- if (index < conn->c_pagedresults.prl_maxlen) {
- count = conn->c_pagedresults.prl_list[index].pr_search_result_set_size_estimate;
- }
-- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ }
- }
- slapi_log_err(SLAPI_LOG_TRACE,
- "pagedresults_get_search_result_set_size_estimate", "<= %d\n",
-@@ -562,10 +634,17 @@ pagedresults_get_search_result_set_size_estimate(Connection *conn,
- return count;
- }
-
-+/*
-+ * Set search result set size estimate for a paged result entry.
-+ *
-+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
-+ * caller already holds pageresult_lock. Never call when holding c_mutex.
-+ */
- int
- pagedresults_set_search_result_set_size_estimate(Connection *conn,
- Operation *op,
- int count,
-+ bool locked,
- int index)
- {
- int rc = -1;
-@@ -576,11 +655,15 @@ pagedresults_set_search_result_set_size_estimate(Connection *conn,
- "pagedresults_set_search_result_set_size_estimate",
- "=> idx=%d\n", index);
- if (conn && (index > -1)) {
-- pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ }
- if (index < conn->c_pagedresults.prl_maxlen) {
- conn->c_pagedresults.prl_list[index].pr_search_result_set_size_estimate = count;
- }
-- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ }
- rc = 0;
- }
- slapi_log_err(SLAPI_LOG_TRACE,
-@@ -589,8 +672,14 @@ pagedresults_set_search_result_set_size_estimate(Connection *conn,
- return rc;
- }
-
-+/*
-+ * Get with_sort flag for a paged result entry.
-+ *
-+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
-+ * caller already holds pageresult_lock. Never call when holding c_mutex.
-+ */
- int
--pagedresults_get_with_sort(Connection *conn, Operation *op, int index)
-+pagedresults_get_with_sort(Connection *conn, Operation *op, bool locked, int index)
- {
- int flags = 0;
- if (!op_is_pagedresults(op)) {
-@@ -599,19 +688,29 @@ pagedresults_get_with_sort(Connection *conn, Operation *op, int index)
- slapi_log_err(SLAPI_LOG_TRACE,
- "pagedresults_get_with_sort", "=> idx=%d\n", index);
- if (conn && (index > -1)) {
-- pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ }
- if (index < conn->c_pagedresults.prl_maxlen) {
- flags = conn->c_pagedresults.prl_list[index].pr_flags & CONN_FLAG_PAGEDRESULTS_WITH_SORT;
- }
-- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ }
- }
- slapi_log_err(SLAPI_LOG_TRACE,
- "pagedresults_get_with_sort", "<= %d\n", flags);
- return flags;
- }
-
-+/*
-+ * Set with_sort flag for a paged result entry.
-+ *
-+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
-+ * caller already holds pageresult_lock. Never call when holding c_mutex.
-+ */
- int
--pagedresults_set_with_sort(Connection *conn, Operation *op, int flags, int index)
-+pagedresults_set_with_sort(Connection *conn, Operation *op, int flags, bool locked, int index)
- {
- int rc = -1;
- if (!op_is_pagedresults(op)) {
-@@ -620,14 +719,18 @@ pagedresults_set_with_sort(Connection *conn, Operation *op, int flags, int index
- slapi_log_err(SLAPI_LOG_TRACE,
- "pagedresults_set_with_sort", "=> idx=%d\n", index);
- if (conn && (index > -1)) {
-- pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
-+ }
- if (index < conn->c_pagedresults.prl_maxlen) {
- if (flags & OP_FLAG_SERVER_SIDE_SORTING) {
- conn->c_pagedresults.prl_list[index].pr_flags |=
- CONN_FLAG_PAGEDRESULTS_WITH_SORT;
- }
- }
-- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ if (!locked) {
-+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-+ }
- rc = 0;
- }
- slapi_log_err(SLAPI_LOG_TRACE, "pagedresults_set_with_sort", "<= %d\n", rc);
-@@ -802,10 +905,6 @@ pagedresults_cleanup(Connection *conn, int needlock)
- rc = 1;
- }
- prp->pr_current_be = NULL;
-- if (prp->pr_mutex) {
-- PR_DestroyLock(prp->pr_mutex);
-- prp->pr_mutex = NULL;
-- }
- memset(prp, '\0', sizeof(PagedResults));
- }
- conn->c_pagedresults.prl_count = 0;
-@@ -840,10 +939,6 @@ pagedresults_cleanup_all(Connection *conn, int needlock)
- i < conn->c_pagedresults.prl_maxlen;
- i++) {
- prp = conn->c_pagedresults.prl_list + i;
-- if (prp->pr_mutex) {
-- PR_DestroyLock(prp->pr_mutex);
-- prp->pr_mutex = NULL;
-- }
- if (prp->pr_current_be && prp->pr_search_result_set &&
- prp->pr_current_be->be_search_results_release) {
- prp->pr_current_be->be_search_results_release(&(prp->pr_search_result_set));
-@@ -1010,43 +1105,8 @@ op_set_pagedresults(Operation *op)
- op->o_flags |= OP_FLAG_PAGED_RESULTS;
- }
-
--/*
-- * pagedresults_lock/unlock -- introduced to protect search results for the
-- * asynchronous searches. Do not call these functions while the PR conn lock
-- * is held (e.g. pageresult_lock_get_addr(conn))
-- */
--void
--pagedresults_lock(Connection *conn, int index)
--{
-- PagedResults *prp;
-- if (!conn || (index < 0) || (index >= conn->c_pagedresults.prl_maxlen)) {
-- return;
-- }
-- pthread_mutex_lock(pageresult_lock_get_addr(conn));
-- prp = conn->c_pagedresults.prl_list + index;
-- if (prp->pr_mutex) {
-- PR_Lock(prp->pr_mutex);
-- }
-- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
--}
--
--void
--pagedresults_unlock(Connection *conn, int index)
--{
-- PagedResults *prp;
-- if (!conn || (index < 0) || (index >= conn->c_pagedresults.prl_maxlen)) {
-- return;
-- }
-- pthread_mutex_lock(pageresult_lock_get_addr(conn));
-- prp = conn->c_pagedresults.prl_list + index;
-- if (prp->pr_mutex) {
-- PR_Unlock(prp->pr_mutex);
-- }
-- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
--}
--
- int
--pagedresults_is_abandoned_or_notavailable(Connection *conn, int locked, int index)
-+pagedresults_is_abandoned_or_notavailable(Connection *conn, bool locked, int index)
- {
- PagedResults *prp;
- int32_t result;
-@@ -1066,7 +1126,7 @@ pagedresults_is_abandoned_or_notavailable(Connection *conn, int locked, int inde
- }
-
- int
--pagedresults_set_search_result_pb(Slapi_PBlock *pb, void *sr, int locked)
-+pagedresults_set_search_result_pb(Slapi_PBlock *pb, void *sr, bool locked)
- {
- int rc = -1;
- Connection *conn = NULL;
-diff --git a/ldap/servers/slapd/proto-slap.h b/ldap/servers/slapd/proto-slap.h
-index 6af6583a5..8445670d2 100644
---- a/ldap/servers/slapd/proto-slap.h
-+++ b/ldap/servers/slapd/proto-slap.h
-@@ -1611,20 +1611,22 @@ pthread_mutex_t *pageresult_lock_get_addr(Connection *conn);
- int pagedresults_parse_control_value(Slapi_PBlock *pb, struct berval *psbvp, ber_int_t *pagesize, int *index, Slapi_Backend *be);
- void pagedresults_set_response_control(Slapi_PBlock *pb, int iscritical, ber_int_t estimate, int curr_search_count, int index);
- Slapi_Backend *pagedresults_get_current_be(Connection *conn, int index);
--int pagedresults_set_current_be(Connection *conn, Slapi_Backend *be, int index, int nolock);
--void *pagedresults_get_search_result(Connection *conn, Operation *op, int locked, int index);
--int pagedresults_set_search_result(Connection *conn, Operation *op, void *sr, int locked, int index);
--int pagedresults_get_search_result_count(Connection *conn, Operation *op, int index);
--int pagedresults_set_search_result_count(Connection *conn, Operation *op, int cnt, int index);
-+int pagedresults_set_current_be(Connection *conn, Slapi_Backend *be, int index, bool locked);
-+void *pagedresults_get_search_result(Connection *conn, Operation *op, bool locked, int index);
-+int pagedresults_set_search_result(Connection *conn, Operation *op, void *sr, bool locked, int index);
-+int pagedresults_get_search_result_count(Connection *conn, Operation *op, bool locked, int index);
-+int pagedresults_set_search_result_count(Connection *conn, Operation *op, int cnt, bool locked, int index);
- int pagedresults_get_search_result_set_size_estimate(Connection *conn,
- Operation *op,
-+ bool locked,
- int index);
- int pagedresults_set_search_result_set_size_estimate(Connection *conn,
- Operation *op,
- int cnt,
-+ bool locked,
- int index);
--int pagedresults_get_with_sort(Connection *conn, Operation *op, int index);
--int pagedresults_set_with_sort(Connection *conn, Operation *op, int flags, int index);
-+int pagedresults_get_with_sort(Connection *conn, Operation *op, bool locked, int index);
-+int pagedresults_set_with_sort(Connection *conn, Operation *op, int flags, bool locked, int index);
- int pagedresults_get_unindexed(Connection *conn, Operation *op, int index);
- int pagedresults_set_unindexed(Connection *conn, Operation *op, int index);
- int pagedresults_get_sort_result_code(Connection *conn, Operation *op, int index);
-@@ -1636,15 +1638,13 @@ int pagedresults_cleanup(Connection *conn, int needlock);
- int pagedresults_is_timedout_nolock(Connection *conn);
- int pagedresults_reset_timedout_nolock(Connection *conn);
- int pagedresults_in_use_nolock(Connection *conn);
--int pagedresults_free_one(Connection *conn, Operation *op, int index);
--int pagedresults_free_one_msgid(Connection *conn, ber_int_t msgid, pthread_mutex_t *mutex);
-+int pagedresults_free_one(Connection *conn, Operation *op, bool locked, int index);
-+int pagedresults_free_one_msgid(Connection *conn, ber_int_t msgid, bool locked);
- int op_is_pagedresults(Operation *op);
- int pagedresults_cleanup_all(Connection *conn, int needlock);
- void op_set_pagedresults(Operation *op);
--void pagedresults_lock(Connection *conn, int index);
--void pagedresults_unlock(Connection *conn, int index);
--int pagedresults_is_abandoned_or_notavailable(Connection *conn, int locked, int index);
--int pagedresults_set_search_result_pb(Slapi_PBlock *pb, void *sr, int locked);
-+int pagedresults_is_abandoned_or_notavailable(Connection *conn, bool locked, int index);
-+int pagedresults_set_search_result_pb(Slapi_PBlock *pb, void *sr, bool locked);
-
- /*
- * sort.c
-diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h
-index 49cfb4210..abb0d2e47 100644
---- a/ldap/servers/slapd/slap.h
-+++ b/ldap/servers/slapd/slap.h
-@@ -89,6 +89,10 @@ static char ptokPBE[34] = "Internal (Software) Token ";
- #include <stdbool.h>
- #include <time.h> /* For timespec definitions */
-
-+/* Macros for paged results lock parameter */
-+#define PR_LOCKED true
-+#define PR_NOT_LOCKED false
-+
- /* Provides our int types and platform specific requirements. */
- #include <slapi_pal.h>
-
-@@ -1669,7 +1673,6 @@ typedef struct _paged_results
- struct timespec pr_timelimit_hr; /* expiry time of this request rel to clock monotonic */
- int pr_flags;
- ber_int_t pr_msgid; /* msgid of the request; to abandon */
-- PRLock *pr_mutex; /* protect each conn structure */
- } PagedResults;
-
- /* array of simple paged structure stashed in connection */
---
-2.52.0
-
diff --git a/0007-Issue-7108-Fix-shutdown-crash-in-entry-cache-destruc.patch b/0007-Issue-7108-Fix-shutdown-crash-in-entry-cache-destruc.patch
deleted file mode 100644
index 3935e95..0000000
--- a/0007-Issue-7108-Fix-shutdown-crash-in-entry-cache-destruc.patch
+++ /dev/null
@@ -1,183 +0,0 @@
-From cde999edf7246d9dcec4a13950e2c0895165a16e Mon Sep 17 00:00:00 2001
-From: Simon Pichugin <spichugi@redhat.com>
-Date: Thu, 8 Jan 2026 10:02:39 -0800
-Subject: [PATCH] Issue 7108 - Fix shutdown crash in entry cache destruction
- (#7163)
-
-Description: The entry cache could experience LRU list corruption when
-using pinned entries, leading to crashes during cache flush operations.
-
-In entrycache_add_int(), when returning an existing cached entry, the
-code checked the wrong entry's state before calling lru_delete(). It
-checked the new entry 'e' but operated on the existing entry 'my_alt',
-causing lru_delete() to be called on entries not in the LRU list. This
-is fixed by checking my_alt's refcnt and pinned state instead.
-
-In flush_hash(), pinned_remove() and lru_delete() were both called on
-pinned entries. Since pinned entries are in the pinned list, calling
-lru_delete() afterwards corrupted the list. This is fixed by calling
-either pinned_remove() or lru_delete() based on the entry's state.
-
-A NULL check is added in entrycache_flush() and dncache_flush() to
-gracefully handle corrupted LRU lists and prevent crashes when
-traversing backwards through the list encounters an unexpected NULL.
-
-Entry pointers are now always cleared after lru_delete() removal to
-prevent stale pointer issues in non-debug builds.
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7108
-
-Reviewed by: @progier389, @vashirov (Thanks!!)
----
- ldap/servers/slapd/back-ldbm/cache.c | 48 +++++++++++++++++++++++++---
- 1 file changed, 43 insertions(+), 5 deletions(-)
-
-diff --git a/ldap/servers/slapd/back-ldbm/cache.c b/ldap/servers/slapd/back-ldbm/cache.c
-index 2e4126134..a87f30687 100644
---- a/ldap/servers/slapd/back-ldbm/cache.c
-+++ b/ldap/servers/slapd/back-ldbm/cache.c
-@@ -458,11 +458,13 @@ static void
- lru_delete(struct cache *cache, void *ptr)
- {
- struct backcommon *e;
-+
- if (NULL == ptr) {
- LOG("=> lru_delete\n<= lru_delete (null entry)\n");
- return;
- }
- e = (struct backcommon *)ptr;
-+
- #ifdef LDAP_CACHE_DEBUG_LRU
- pinned_verify(cache, __LINE__);
- lru_verify(cache, e, 1);
-@@ -475,8 +477,9 @@ lru_delete(struct cache *cache, void *ptr)
- e->ep_lrunext->ep_lruprev = e->ep_lruprev;
- else
- cache->c_lrutail = e->ep_lruprev;
--#ifdef LDAP_CACHE_DEBUG_LRU
-+ /* Always clear pointers after removal to prevent stale pointer issues */
- e->ep_lrunext = e->ep_lruprev = NULL;
-+#ifdef LDAP_CACHE_DEBUG_LRU
- lru_verify(cache, e, 0);
- #endif
- }
-@@ -633,9 +636,14 @@ flush_hash(struct cache *cache, struct timespec *start_time, int32_t type)
- if (entry->ep_refcnt == 0) {
- entry->ep_refcnt++;
- if (entry->ep_state & ENTRY_STATE_PINNED) {
-+ /* Entry is in pinned list, not LRU - remove from pinned only.
-+ * pinned_remove clears lru pointers and won't add to LRU since refcnt > 0.
-+ */
- pinned_remove(cache, laste);
-+ } else {
-+ /* Entry is in LRU list - remove from LRU */
-+ lru_delete(cache, laste);
- }
-- lru_delete(cache, laste);
- if (type == ENTRY_CACHE) {
- entrycache_remove_int(cache, laste);
- entrycache_return(cache, (struct backentry **)&laste, PR_TRUE);
-@@ -679,9 +687,14 @@ flush_hash(struct cache *cache, struct timespec *start_time, int32_t type)
- if (entry->ep_refcnt == 0) {
- entry->ep_refcnt++;
- if (entry->ep_state & ENTRY_STATE_PINNED) {
-+ /* Entry is in pinned list, not LRU - remove from pinned only.
-+ * pinned_remove clears lru pointers and won't add to LRU since refcnt > 0.
-+ */
- pinned_remove(cache, laste);
-+ } else {
-+ /* Entry is in LRU list - remove from LRU */
-+ lru_delete(cache, laste);
- }
-- lru_delete(cache, laste);
- entrycache_remove_int(cache, laste);
- entrycache_return(cache, (struct backentry **)&laste, PR_TRUE);
- } else {
-@@ -772,6 +785,11 @@ entrycache_flush(struct cache *cache)
- } else {
- e = BACK_LRU_PREV(e, struct backentry *);
- }
-+ if (e == NULL) {
-+ slapi_log_err(SLAPI_LOG_WARNING, "entrycache_flush",
-+ "Unexpected NULL entry while flushing cache - LRU list may be corrupted\n");
-+ break;
-+ }
- ASSERT(e->ep_refcnt == 0);
- e->ep_refcnt++;
- if (entrycache_remove_int(cache, e) < 0) {
-@@ -1160,6 +1178,7 @@ pinned_remove(struct cache *cache, void *ptr)
- {
- struct backentry *e = (struct backentry *)ptr;
- ASSERT(e->ep_state & ENTRY_STATE_PINNED);
-+
- cache->c_pinned_ctx->npinned--;
- cache->c_pinned_ctx->size -= e->ep_size;
- e->ep_state &= ~ENTRY_STATE_PINNED;
-@@ -1172,13 +1191,23 @@ pinned_remove(struct cache *cache, void *ptr)
- cache->c_pinned_ctx->head = cache->c_pinned_ctx->tail = NULL;
- } else {
- cache->c_pinned_ctx->head = BACK_LRU_NEXT(e, struct backentry *);
-+ /* Update new head's prev pointer to NULL */
-+ if (cache->c_pinned_ctx->head) {
-+ cache->c_pinned_ctx->head->ep_lruprev = NULL;
-+ }
- }
- } else if (cache->c_pinned_ctx->tail == e) {
- cache->c_pinned_ctx->tail = BACK_LRU_PREV(e, struct backentry *);
-+ /* Update new tail's next pointer to NULL */
-+ if (cache->c_pinned_ctx->tail) {
-+ cache->c_pinned_ctx->tail->ep_lrunext = NULL;
-+ }
- } else {
-+ /* Middle of list: update both neighbors to point to each other */
- BACK_LRU_PREV(e, struct backentry *)->ep_lrunext = BACK_LRU_NEXT(e, struct backcommon *);
- BACK_LRU_NEXT(e, struct backentry *)->ep_lruprev = BACK_LRU_PREV(e, struct backcommon *);
- }
-+ /* Clear the removed entry's pointers */
- e->ep_lrunext = e->ep_lruprev = NULL;
- if (e->ep_refcnt == 0) {
- lru_add(cache, ptr);
-@@ -1245,6 +1274,7 @@ pinned_add(struct cache *cache, void *ptr)
- return false;
- }
- /* Now it is time to insert the entry in the pinned list */
-+
- cache->c_pinned_ctx->npinned++;
- cache->c_pinned_ctx->size += e->ep_size;
- e->ep_state |= ENTRY_STATE_PINNED;
-@@ -1754,7 +1784,7 @@ entrycache_add_int(struct cache *cache, struct backentry *e, int state, struct b
- * 3) ep_state: 0 && state: 0
- * ==> increase the refcnt
- */
-- if (e->ep_refcnt == 0)
-+ if (e->ep_refcnt == 0 && (e->ep_state & ENTRY_STATE_PINNED) == 0)
- lru_delete(cache, (void *)e);
- e->ep_refcnt++;
- e->ep_state &= ~ENTRY_STATE_UNAVAILABLE;
-@@ -1781,7 +1811,7 @@ entrycache_add_int(struct cache *cache, struct backentry *e, int state, struct b
- } else {
- if (alt) {
- *alt = my_alt;
-- if (e->ep_refcnt == 0 && (e->ep_state & ENTRY_STATE_PINNED) == 0)
-+ if (my_alt->ep_refcnt == 0 && (my_alt->ep_state & ENTRY_STATE_PINNED) == 0)
- lru_delete(cache, (void *)*alt);
- (*alt)->ep_refcnt++;
- LOG("the entry %s already exists. returning existing entry %s (state: 0x%x)\n",
-@@ -2379,6 +2409,14 @@ dncache_flush(struct cache *cache)
- } else {
- dn = BACK_LRU_PREV(dn, struct backdn *);
- }
-+ if (dn == NULL) {
-+ /* Safety check: we should normally exit via the CACHE_LRU_HEAD check.
-+ * If we get here, c_lruhead may be NULL or the LRU list is corrupted.
-+ */
-+ slapi_log_err(SLAPI_LOG_WARNING, "dncache_flush",
-+ "Unexpected NULL entry while flushing cache - LRU list may be corrupted\n");
-+ break;
-+ }
- ASSERT(dn->ep_refcnt == 0);
- dn->ep_refcnt++;
- if (dncache_remove_int(cache, dn) < 0) {
---
-2.52.0
-
diff --git a/0008-Issue-7172-Index-ordering-mismatch-after-upgrade-717.patch b/0008-Issue-7172-Index-ordering-mismatch-after-upgrade-717.patch
deleted file mode 100644
index 038c6c3..0000000
--- a/0008-Issue-7172-Index-ordering-mismatch-after-upgrade-717.patch
+++ /dev/null
@@ -1,215 +0,0 @@
-From 062aa6eab12d00adffa4e46d58722f6c0e5eeac1 Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Fri, 9 Jan 2026 11:39:50 +0100
-Subject: [PATCH] Issue 7172 - Index ordering mismatch after upgrade (#7173)
-
-Bug Description:
-Commit daf731f55071d45eaf403a52b63d35f4e699ff28 introduced a regression.
-After upgrading to a version that adds `integerOrderingMatch` matching
-rule to `parentid` and `ancestorid` indexes, searches may return empty
-or incorrect results.
-
-This happens because the existing index data was created with
-lexicographic ordering, but the new compare function expects integer
-ordering. Index lookups fail because the compare function doesn't match
-the data ordering.
-The root cause is that `ldbm_instance_create_default_indexes()` calls
-`attr_index_config()` unconditionally for `parentid` and `ancestorid`
-indexes, which triggers `ainfo_dup()` to overwrite `ai_key_cmp_fn` on
-existing indexes. This breaks indexes that were created without the
-`integerOrderingMatch` matching rule.
-
-Fix Description:
-* Call `attr_index_config()` for `parentid` and `ancestorid` indexes
-only if index config doesn't exist.
-
-* Add `upgrade_check_id_index_matching_rule()` that logs an error on
-server startup if `parentid` or `ancestorid` indexes are missing the
-integerOrderingMatch matching rule, advising administrators to reindex.
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7172
-
-Reviewed by: @tbordaz, @progier389, @droideck (Thanks!)
----
- ldap/servers/slapd/back-ldbm/instance.c | 25 ++++--
- ldap/servers/slapd/upgrade.c | 107 +++++++++++++++++++++++-
- 2 files changed, 123 insertions(+), 9 deletions(-)
-
-diff --git a/ldap/servers/slapd/back-ldbm/instance.c b/ldap/servers/slapd/back-ldbm/instance.c
-index cb002c379..71bf0f6fa 100644
---- a/ldap/servers/slapd/back-ldbm/instance.c
-+++ b/ldap/servers/slapd/back-ldbm/instance.c
-@@ -190,6 +190,7 @@ ldbm_instance_create_default_indexes(backend *be)
- char *ancestorid_indexes_limit = NULL;
- char *parentid_indexes_limit = NULL;
- struct attrinfo *ai = NULL;
-+ struct attrinfo *index_already_configured = NULL;
- struct index_idlistsizeinfo *iter;
- int cookie;
- int limit;
-@@ -248,10 +249,14 @@ ldbm_instance_create_default_indexes(backend *be)
- ldbm_instance_config_add_index_entry(inst, e, flags);
- slapi_entry_free(e);
-
-- e = ldbm_instance_init_config_entry(LDBM_PARENTID_STR, "eq", 0, 0, 0, "integerOrderingMatch", parentid_indexes_limit);
-- ldbm_instance_config_add_index_entry(inst, e, flags);
-- attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
-- slapi_entry_free(e);
-+ ainfo_get(be, (char *)LDBM_PARENTID_STR, &ai);
-+ index_already_configured = ai;
-+ if (!index_already_configured) {
-+ e = ldbm_instance_init_config_entry(LDBM_PARENTID_STR, "eq", 0, 0, 0, "integerOrderingMatch", parentid_indexes_limit);
-+ ldbm_instance_config_add_index_entry(inst, e, flags);
-+ attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
-+ slapi_entry_free(e);
-+ }
-
- e = ldbm_instance_init_config_entry("objectclass", "eq", 0, 0, 0, 0, 0);
- ldbm_instance_config_add_index_entry(inst, e, flags);
-@@ -288,10 +293,14 @@ ldbm_instance_create_default_indexes(backend *be)
- * ancestorid is special, there is actually no such attr type
- * but we still want to use the attr index file APIs.
- */
-- e = ldbm_instance_init_config_entry(LDBM_ANCESTORID_STR, "eq", 0, 0, 0, "integerOrderingMatch", ancestorid_indexes_limit);
-- ldbm_instance_config_add_index_entry(inst, e, flags);
-- attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
-- slapi_entry_free(e);
-+ ainfo_get(be, (char *)LDBM_ANCESTORID_STR, &ai);
-+ index_already_configured = ai;
-+ if (!index_already_configured) {
-+ e = ldbm_instance_init_config_entry(LDBM_ANCESTORID_STR, "eq", 0, 0, 0, "integerOrderingMatch", ancestorid_indexes_limit);
-+ ldbm_instance_config_add_index_entry(inst, e, flags);
-+ attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
-+ slapi_entry_free(e);
-+ }
-
- slapi_ch_free_string(&ancestorid_indexes_limit);
- slapi_ch_free_string(&parentid_indexes_limit);
-diff --git a/ldap/servers/slapd/upgrade.c b/ldap/servers/slapd/upgrade.c
-index 858392564..b02e37ed6 100644
---- a/ldap/servers/slapd/upgrade.c
-+++ b/ldap/servers/slapd/upgrade.c
-@@ -330,6 +330,107 @@ upgrade_remove_subtree_rename(void)
- return UPGRADE_SUCCESS;
- }
-
-+/*
-+ * Check if parentid/ancestorid indexes are missing the integerOrderingMatch
-+ * matching rule.
-+ *
-+ * This function logs a warning if we detect this condition, advising
-+ * the administrator to reindex the affected attributes.
-+ */
-+static upgrade_status
-+upgrade_check_id_index_matching_rule(void)
-+{
-+ struct slapi_pblock *pb = slapi_pblock_new();
-+ Slapi_Entry **backends = NULL;
-+ const char *be_base_dn = "cn=ldbm database,cn=plugins,cn=config";
-+ const char *be_filter = "(objectclass=nsBackendInstance)";
-+ const char *attrs_to_check[] = {"parentid", "ancestorid", NULL};
-+ upgrade_status uresult = UPGRADE_SUCCESS;
-+
-+ /* Search for all backend instances */
-+ slapi_search_internal_set_pb(
-+ pb, be_base_dn,
-+ LDAP_SCOPE_ONELEVEL,
-+ be_filter, NULL, 0, NULL, NULL,
-+ plugin_get_default_component_id(), 0);
-+ slapi_search_internal_pb(pb);
-+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &backends);
-+
-+ if (backends) {
-+ for (size_t be_idx = 0; backends[be_idx] != NULL; be_idx++) {
-+ const char *be_name = slapi_entry_attr_get_ref(backends[be_idx], "cn");
-+ if (!be_name) {
-+ continue;
-+ }
-+
-+ /* Check each attribute that should have integerOrderingMatch */
-+ for (size_t attr_idx = 0; attrs_to_check[attr_idx] != NULL; attr_idx++) {
-+ const char *attr_name = attrs_to_check[attr_idx];
-+ struct slapi_pblock *idx_pb = slapi_pblock_new();
-+ Slapi_Entry **idx_entries = NULL;
-+ char *idx_dn = slapi_create_dn_string("cn=%s,cn=index,cn=%s,%s",
-+ attr_name, be_name, be_base_dn);
-+ char *idx_filter = "(objectclass=nsIndex)";
-+ PRBool has_matching_rule = PR_FALSE;
-+
-+ if (!idx_dn) {
-+ slapi_pblock_destroy(idx_pb);
-+ continue;
-+ }
-+
-+ slapi_search_internal_set_pb(
-+ idx_pb, idx_dn,
-+ LDAP_SCOPE_BASE,
-+ idx_filter, NULL, 0, NULL, NULL,
-+ plugin_get_default_component_id(), 0);
-+ slapi_search_internal_pb(idx_pb);
-+ slapi_pblock_get(idx_pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &idx_entries);
-+
-+ if (idx_entries && idx_entries[0]) {
-+ /* Index exists, check if it has integerOrderingMatch */
-+ Slapi_Attr *mr_attr = NULL;
-+ if (slapi_entry_attr_find(idx_entries[0], "nsMatchingRule", &mr_attr) == 0) {
-+ Slapi_Value *sval = NULL;
-+ int idx;
-+ for (idx = slapi_attr_first_value(mr_attr, &sval);
-+ idx != -1;
-+ idx = slapi_attr_next_value(mr_attr, idx, &sval)) {
-+ const struct berval *bval = slapi_value_get_berval(sval);
-+ if (bval && bval->bv_val &&
-+ strcasecmp(bval->bv_val, "integerOrderingMatch") == 0) {
-+ has_matching_rule = PR_TRUE;
-+ break;
-+ }
-+ }
-+ }
-+
-+ if (!has_matching_rule) {
-+ /* Index exists but doesn't have integerOrderingMatch, log a warning */
-+ slapi_log_err(SLAPI_LOG_ERR, "upgrade_check_id_index_matching_rule",
-+ "Index '%s' in backend '%s' is missing 'nsMatchingRule: integerOrderingMatch'. "
-+ "Incorrectly configured system indexes can lead to poor search performance, replication issues, and other operational problems. "
-+ "To fix this, add the matching rule and reindex: "
-+ "dsconf <instance> backend index set --add-mr integerOrderingMatch --attr %s %s && "
-+ "dsconf <instance> backend index reindex --attr %s %s. "
-+ "WARNING: Reindexing can be resource-intensive and may impact server performance on a live system. "
-+ "Consider scheduling reindexing during maintenance windows or periods of low activity.\n",
-+ attr_name, be_name, attr_name, be_name, attr_name, be_name);
-+ }
-+ }
-+
-+ slapi_ch_free_string(&idx_dn);
-+ slapi_free_search_results_internal(idx_pb);
-+ slapi_pblock_destroy(idx_pb);
-+ }
-+ }
-+ }
-+
-+ slapi_free_search_results_internal(pb);
-+ slapi_pblock_destroy(pb);
-+
-+ return uresult;
-+}
-+
- /*
- * Upgrade the base config of the PAM PTA plugin.
- *
-@@ -547,7 +648,11 @@ upgrade_server(void)
- if (upgrade_pam_pta_default_config() != UPGRADE_SUCCESS) {
- return UPGRADE_FAILURE;
- }
--
-+
-+ if (upgrade_check_id_index_matching_rule() != UPGRADE_SUCCESS) {
-+ return UPGRADE_FAILURE;
-+ }
-+
- return UPGRADE_SUCCESS;
- }
-
---
-2.52.0
-
diff --git a/0009-Issue-7172-2nd-Index-ordering-mismatch-after-upgrade.patch b/0009-Issue-7172-2nd-Index-ordering-mismatch-after-upgrade.patch
deleted file mode 100644
index 442fb27..0000000
--- a/0009-Issue-7172-2nd-Index-ordering-mismatch-after-upgrade.patch
+++ /dev/null
@@ -1,67 +0,0 @@
-From 6bce8f6e8c985289c4ac1a4f051c291283c0a1ec Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Mon, 12 Jan 2026 10:58:02 +0100
-Subject: [PATCH 9/9] Issue 7172 - (2nd) Index ordering mismatch after upgrade
- (#7180)
-
-Commit 742c12e0247ab64e87da000a4de2f3e5c99044ab introduced a regression
-where the check to skip creating parentid/ancestorid indexes if they
-already exist was incorrect.
-The `ainfo_get()` function falls back to returning
-LDBM_PSEUDO_ATTR_DEFAULT attrinfo when the requested attribute is not
-found.
-Since LDBM_PSEUDO_ATTR_DEFAULT is created before the ancestorid check,
-`ainfo_get()` returns LDBM_PSEUDO_ATTR_DEFAULT instead of NULL, causing
-the ancestorid index creation to be skipped entirely.
-
-When operations later try to use the ancestorid index, they fall back to
-LDBM_PSEUDO_ATTR_DEFAULT, and attempting to open the .default dbi
-mid-transaction fails with MDB_NOTFOUND (-30798).
-
-Fix Description:
-Instead of just checking if `ainfo_get()` returns non-NULL, verify that
-the returned attrinfo is actually for the requested attribute.
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7172
-
-Reviewed by: @tbordaz (Thanks!)
----
- ldap/servers/slapd/back-ldbm/instance.c | 8 +++++---
- 1 file changed, 5 insertions(+), 3 deletions(-)
-
-diff --git a/ldap/servers/slapd/back-ldbm/instance.c b/ldap/servers/slapd/back-ldbm/instance.c
-index 71bf0f6fa..2a6e8cbb8 100644
---- a/ldap/servers/slapd/back-ldbm/instance.c
-+++ b/ldap/servers/slapd/back-ldbm/instance.c
-@@ -190,7 +190,7 @@ ldbm_instance_create_default_indexes(backend *be)
- char *ancestorid_indexes_limit = NULL;
- char *parentid_indexes_limit = NULL;
- struct attrinfo *ai = NULL;
-- struct attrinfo *index_already_configured = NULL;
-+ int index_already_configured = 0;
- struct index_idlistsizeinfo *iter;
- int cookie;
- int limit;
-@@ -250,7 +250,8 @@ ldbm_instance_create_default_indexes(backend *be)
- slapi_entry_free(e);
-
- ainfo_get(be, (char *)LDBM_PARENTID_STR, &ai);
-- index_already_configured = ai;
-+ /* Check if the attrinfo is actually for parentid, not a fallback to .default */
-+ index_already_configured = (ai != NULL && strcmp(ai->ai_type, LDBM_PARENTID_STR) == 0);
- if (!index_already_configured) {
- e = ldbm_instance_init_config_entry(LDBM_PARENTID_STR, "eq", 0, 0, 0, "integerOrderingMatch", parentid_indexes_limit);
- ldbm_instance_config_add_index_entry(inst, e, flags);
-@@ -294,7 +295,8 @@ ldbm_instance_create_default_indexes(backend *be)
- * but we still want to use the attr index file APIs.
- */
- ainfo_get(be, (char *)LDBM_ANCESTORID_STR, &ai);
-- index_already_configured = ai;
-+ /* Check if the attrinfo is actually for ancestorid, not a fallback to .default */
-+ index_already_configured = (ai != NULL && strcmp(ai->ai_type, LDBM_ANCESTORID_STR) == 0);
- if (!index_already_configured) {
- e = ldbm_instance_init_config_entry(LDBM_ANCESTORID_STR, "eq", 0, 0, 0, "integerOrderingMatch", ancestorid_indexes_limit);
- ldbm_instance_config_add_index_entry(inst, e, flags);
---
-2.52.0
-
diff --git a/0010-Bump-lodash-from-4.17.21-to-4.17.23-in-src-cockpit-3.patch b/0010-Bump-lodash-from-4.17.21-to-4.17.23-in-src-cockpit-3.patch
deleted file mode 100644
index d90aa5d..0000000
--- a/0010-Bump-lodash-from-4.17.21-to-4.17.23-in-src-cockpit-3.patch
+++ /dev/null
@@ -1,56 +0,0 @@
-From a6d18806b5b65aba5c0e0b686619de782e420d65 Mon Sep 17 00:00:00 2001
-From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
-Date: Wed, 21 Jan 2026 19:58:46 -0800
-Subject: [PATCH] Bump lodash from 4.17.21 to 4.17.23 in
- /src/cockpit/389-console (#7203)
-
-Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
-- [Release notes](https://github.com/lodash/lodash/releases)
-- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)
-
----
-updated-dependencies:
-- dependency-name: lodash
- dependency-version: 4.17.23
- dependency-type: indirect
-...
-
-Signed-off-by: dependabot[bot] <support@github.com>
-Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
----
- src/cockpit/389-console/package-lock.json | 12 ++++++------
- 1 file changed, 6 insertions(+), 6 deletions(-)
-
-diff --git a/src/cockpit/389-console/package-lock.json b/src/cockpit/389-console/package-lock.json
-index 0aa5bbbb9..23faef62f 100644
---- a/src/cockpit/389-console/package-lock.json
-+++ b/src/cockpit/389-console/package-lock.json
-@@ -4833,9 +4833,9 @@
- }
- },
- "node_modules/lodash": {
-- "version": "4.17.21",
-- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
-+ "version": "4.17.23",
-+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
-+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="
- },
- "node_modules/lodash.merge": {
- "version": "4.6.2",
-@@ -11087,9 +11087,9 @@
- }
- },
- "lodash": {
-- "version": "4.17.21",
-- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
-+ "version": "4.17.23",
-+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
-+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="
- },
- "lodash.merge": {
- "version": "4.6.2",
---
-2.52.0
-
diff --git a/0011-Issue-7189-DSBLE0007-generates-incorrect-remediation.patch b/0011-Issue-7189-DSBLE0007-generates-incorrect-remediation.patch
deleted file mode 100644
index a4d01cc..0000000
--- a/0011-Issue-7189-DSBLE0007-generates-incorrect-remediation.patch
+++ /dev/null
@@ -1,235 +0,0 @@
-From f4a899a7b7764059da56be2835d0b3e93b4a84b4 Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Tue, 20 Jan 2026 09:52:47 +0100
-Subject: [PATCH] Issue 7189 - DSBLE0007 generates incorrect remediation
- commands for scan limits
-
-Bug Description:
-
-The generated dsconf commands for fixing missing system indexes had two issues:
-
-1. The --add-scanlimit value was not quoted, causing the shell to interpret
- "limit=5000 type=eq flags=AND" as multiple arguments instead of a single
- value, resulting in "unrecognized arguments: type=eq flags=AND" error.
-
-2. When both matching rule and scanlimit were missing, two separate commands
- were generated where the second would fail because the matching rule was
- already added by the first command.
-
-Fix Description:
-
-1. Quote the scanlimit value in all remediation commands
-
-2. Combine matching rule and scanlimit fixes into a single command when
- both are missing for the same index instead of expected_scanlimit)
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7189
-
-Reviewed by: @progier389, @droideck (Thanks!)
----
- .../healthcheck/health_system_indexes_test.py | 126 ++++++++++++++++++
- src/lib389/lib389/backend.py | 39 +++---
- 2 files changed, 147 insertions(+), 18 deletions(-)
-
-diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-index a977b71d1..486fad44b 100644
---- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-@@ -408,6 +408,132 @@ def test_retrocl_plugin_missing_matching_rule(topology_st, retrocl_plugin_enable
- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
-
-
-+def test_missing_scanlimit(topology_st, log_buffering_enabled):
-+ """Check if healthcheck returns DSBLE0007 code when parentId index is missing scanlimit
-+
-+ :id: 40e1bf6a-2397-459b-bdf3-f787ca118b86
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Remove nsIndexIDListScanLimit from parentId index
-+ 3. Use healthcheck without --json option
-+ 4. Use healthcheck with --json option
-+ 5. Verify the remediation command has properly quoted scanlimit
-+ 6. Re-add the scanlimit
-+ 7. Use healthcheck without --json option
-+ 8. Use healthcheck with --json option
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. healthcheck reports DSBLE0007 code and related details
-+ 4. healthcheck reports DSBLE0007 code and related details
-+ 5. The scanlimit value is quoted in the remediation command
-+ 6. Success
-+ 7. healthcheck reports no issues found
-+ 8. healthcheck reports no issues found
-+ """
-+
-+ RET_CODE = "DSBLE0007"
-+ PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
-+ SCANLIMIT_VALUE = "limit=5000 type=eq flags=AND"
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Remove nsIndexIDListScanLimit from parentId index")
-+ parentid_index = Index(standalone, PARENTID_DN)
-+ parentid_index.remove("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
-+
-+ run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=RET_CODE)
-+
-+ # Verify the remediation command has properly quoted scanlimit
-+ args = FakeArgs()
-+ args.instance = standalone.serverid
-+ args.verbose = standalone.verbose
-+ args.list_errors = False
-+ args.list_checks = False
-+ args.exclude_check = []
-+ args.check = ["backends"]
-+ args.dry_run = False
-+ args.json = False
-+ health_check_run(standalone, topology_st.logcap.log, args)
-+ # Check that the scanlimit is quoted in the output
-+ assert topology_st.logcap.contains('--add-scanlimit "limit=5000 type=eq flags=AND"')
-+ log.info("Verified scanlimit is properly quoted in remediation command")
-+ topology_st.logcap.flush()
-+
-+ run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=RET_CODE)
-+
-+ log.info("Re-add the nsIndexIDListScanLimit")
-+ parentid_index = Index(standalone, PARENTID_DN)
-+ parentid_index.add("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
-+
-+ run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
-+ run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
-+
-+
-+def test_missing_matching_rule_and_scanlimit(topology_st, log_buffering_enabled):
-+ """Check if healthcheck generates a single combined command when both matching rule and scanlimit are missing
-+
-+ :id: af8214ad-5e4c-422a-8f74-3e99227551df
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Remove both integerOrderingMatch and nsIndexIDListScanLimit from parentId index
-+ 3. Use healthcheck and verify a single combined command is generated
-+ 4. Re-add the matching rule and scanlimit
-+ 5. Use healthcheck without --json option
-+ 6. Use healthcheck with --json option
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. healthcheck reports DSBLE0007 and generates a single command with both --add-mr and --add-scanlimit
-+ 4. Success
-+ 5. healthcheck reports no issues found
-+ 6. healthcheck reports no issues found
-+ """
-+
-+ RET_CODE = "DSBLE0007"
-+ PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
-+ SCANLIMIT_VALUE = "limit=5000 type=eq flags=AND"
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Remove both integerOrderingMatch and nsIndexIDListScanLimit from parentId index")
-+ parentid_index = Index(standalone, PARENTID_DN)
-+ parentid_index.remove("nsMatchingRule", "integerOrderingMatch")
-+ parentid_index.remove("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
-+
-+ # Run healthcheck and verify combined command
-+ args = FakeArgs()
-+ args.instance = standalone.serverid
-+ args.verbose = standalone.verbose
-+ args.list_errors = False
-+ args.list_checks = False
-+ args.exclude_check = []
-+ args.check = ["backends"]
-+ args.dry_run = False
-+ args.json = False
-+ health_check_run(standalone, topology_st.logcap.log, args)
-+
-+ # Verify DSBLE0007 is reported
-+ assert topology_st.logcap.contains(RET_CODE)
-+ log.info("healthcheck returned code: %s" % RET_CODE)
-+
-+ # Verify a single combined command is generated with both --add-mr and --add-scanlimit
-+ assert topology_st.logcap.contains('--add-mr integerOrderingMatch --add-scanlimit "limit=5000 type=eq flags=AND"')
-+ log.info("Verified combined command with both --add-mr and --add-scanlimit")
-+
-+ topology_st.logcap.flush()
-+
-+ log.info("Re-add the integerOrderingMatch matching rule and scanlimit")
-+ parentid_index = Index(standalone, PARENTID_DN)
-+ parentid_index.add("nsMatchingRule", "integerOrderingMatch")
-+ parentid_index.add("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
-+
-+ run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
-+ run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
-+
-+
- def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
- """Check if healthcheck returns DSBLE0007 code when multiple system indexes are missing
-
-diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
-index fba95987b..db464b43a 100644
---- a/src/lib389/lib389/backend.py
-+++ b/src/lib389/lib389/backend.py
-@@ -678,7 +678,7 @@ class Backend(DSLdapObject):
- if expected_config.get('matching_rule'):
- cmd += f" --matching-rule {expected_config['matching_rule']}"
- if expected_config.get('scanlimit'):
-- cmd += f" --add-scanlimit {expected_config['scanlimit']}"
-+ cmd += f" --add-scanlimit \"{expected_config['scanlimit']}\""
- remediation_commands.append(cmd)
- reindex_attrs.add(attr_name) # New index needs reindexing
- else:
-@@ -700,28 +700,31 @@ class Backend(DSLdapObject):
- remediation_commands.append(cmd)
- reindex_attrs.add(attr_name)
-
-- # Check matching rules
-+ # Check matching rules and scanlimit together to generate a single combined command
- expected_mr = expected_config.get('matching_rule')
-+ expected_scanlimit = expected_config.get('scanlimit')
-+
-+ missing_mr = False
- if expected_mr:
- actual_mrs_lower = [mr.lower() for mr in actual_mrs]
- if expected_mr.lower() not in actual_mrs_lower:
- discrepancies.append(f"Index {attr_name} missing matching rule: {expected_mr}")
-- # Add the missing matching rule
-- cmd = f"dsconf YOUR_INSTANCE backend index set {bename} --attr {attr_name} --add-mr {expected_mr}"
-- remediation_commands.append(cmd)
-- reindex_attrs.add(attr_name)
--
-- # Check fine grain definitions for parentid ONLY
-- expected_scanlimit = expected_config.get('scanlimit')
-- if (attr_name.lower() == "parentid") and expected_scanlimit and (len(actual_scanlimit) == 0):
-- discrepancies.append(f"Index {attr_name} missing fine grain definition of IDs limit: {expected_mr}")
-- # Add the missing scanlimit
-- if expected_mr:
-- cmd = f"dsconf YOUR_INSTANCE backend index set {bename} --attr {attr_name} --add-mr {expected_mr} --add-scanlimit {expected_scanlimit}"
-- else:
-- cmd = f"dsconf YOUR_INSTANCE backend index set {bename} --attr {attr_name} --add-scanlimit {expected_scanlimit}"
-- remediation_commands.append(cmd)
-- reindex_attrs.add(attr_name)
-+ missing_mr = True
-+
-+ missing_scanlimit = False
-+ if expected_scanlimit and (len(actual_scanlimit) == 0):
-+ discrepancies.append(f"Index {attr_name} missing fine grain definition of IDs limit: {expected_scanlimit}")
-+ missing_scanlimit = True
-+
-+ # Generate a single combined command for all missing items
-+ if missing_mr or missing_scanlimit:
-+ cmd = f"dsconf YOUR_INSTANCE backend index set {bename} --attr {attr_name}"
-+ if missing_mr:
-+ cmd += f" --add-mr {expected_mr}"
-+ if missing_scanlimit:
-+ cmd += f" --add-scanlimit \"{expected_scanlimit}\""
-+ remediation_commands.append(cmd)
-+ reindex_attrs.add(attr_name)
-
- except Exception as e:
- self._log.debug(f"_lint_system_indexes - Error checking index {attr_name}: {e}")
---
-2.52.0
-
diff --git a/0012-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch b/0012-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch
deleted file mode 100644
index 0043373..0000000
--- a/0012-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch
+++ /dev/null
@@ -1,517 +0,0 @@
-From 8d87a9dba330e8a584bcc9dc9710099bcf720339 Mon Sep 17 00:00:00 2001
-From: Simon Pichugin <spichugi@redhat.com>
-Date: Fri, 23 Jan 2026 17:35:45 -0800
-Subject: [PATCH] Issue 7198 - Web console doesn't show sub-suffix when
- parent-suffix points to an entry (#7202)
-
-Description: The web console doesn't show sub-suffixes when the
-nsslapd-parent-suffix attribute points to an entry rather than a backend
-suffix.
-For example, creating a sub-suffix ou=foo,ou=people,dc=example,dc=com
-with parent-suffix ou=people,dc=example,dc=com (where ou=people is just an
-entry, not a suffix) would not appear in the web console tree.
-
-Fix: In backend_build_tree() and get_sub_suffixes(), the code only matched
-when nsslapd-parent-suffix exactly equaled an existing backend suffix.
-Now it also checks if the parent-suffix is an entry under the current
-suffix (ends with ,suffix) and is not itself a backend suffix. This
-correctly attaches sub-suffixes to their containing suffix when the
-parent-suffix points to an intermediate entry.
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7198
-
-Reviewed by: @progier389 (Thanks!)
----
- .../suites/lib389/subsuffix_tree_test.py | 313 ++++++++++++++++++
- src/lib389/lib389/backend.py | 47 ++-
- src/lib389/lib389/cli_conf/backend.py | 34 +-
- 3 files changed, 370 insertions(+), 24 deletions(-)
- create mode 100644 dirsrvtests/tests/suites/lib389/subsuffix_tree_test.py
-
-diff --git a/dirsrvtests/tests/suites/lib389/subsuffix_tree_test.py b/dirsrvtests/tests/suites/lib389/subsuffix_tree_test.py
-new file mode 100644
-index 000000000..fa10ba530
---- /dev/null
-+++ b/dirsrvtests/tests/suites/lib389/subsuffix_tree_test.py
-@@ -0,0 +1,313 @@
-+# --- BEGIN COPYRIGHT BLOCK ---
-+# Copyright (C) 2026 Red Hat, Inc.
-+# All rights reserved.
-+#
-+# License: GPL (version 3 or any later version).
-+# See LICENSE for details.
-+# --- END COPYRIGHT BLOCK ---
-+#
-+import logging
-+import os
-+import pytest
-+from lib389.topologies import topology_st as topo
-+from lib389.backend import Backends
-+from lib389.idm.organizationalunit import OrganizationalUnits
-+from lib389._constants import DEFAULT_SUFFIX
-+
-+pytestmark = pytest.mark.tier1
-+
-+logging.getLogger(__name__).setLevel(logging.INFO)
-+log = logging.getLogger(__name__)
-+
-+
-+@pytest.fixture(scope="function")
-+def setup_subsuffix_with_entry_parent(topo, request):
-+ """Setup a sub-suffix whose parent-suffix points to an entry, not a suffix."""
-+ inst = topo.standalone
-+
-+ # Create ou=people entry under the root suffix
-+ log.info("Creating ou=people,dc=example,dc=com entry")
-+ ous = OrganizationalUnits(inst, DEFAULT_SUFFIX)
-+ ou_people = ous.get('people')
-+
-+ # Create sub-suffix with parent-suffix pointing to the entry
-+ log.info("Creating sub-suffix ou=foo,ou=people,dc=example,dc=com")
-+ backends = Backends(inst)
-+ subsuffix_dn = 'ou=foo,ou=people,dc=example,dc=com'
-+ parent_suffix_dn = 'ou=people,dc=example,dc=com'
-+
-+ foo_backend = backends.create(properties={
-+ 'cn': 'foo',
-+ 'nsslapd-suffix': subsuffix_dn,
-+ 'parent': parent_suffix_dn,
-+ })
-+
-+ # Create the suffix entry
-+ foo_ous = OrganizationalUnits(inst, parent_suffix_dn)
-+ foo_ou = foo_ous.create(properties={'ou': 'foo'})
-+
-+ def cleanup():
-+ log.info("Cleaning up test backends and entries")
-+ try:
-+ foo_ou.delete()
-+ except Exception as e:
-+ log.warning(f"Failed to delete foo_ou: {e}")
-+ try:
-+ foo_backend.delete()
-+ except Exception as e:
-+ log.warning(f"Failed to delete foo_backend: {e}")
-+
-+ request.addfinalizer(cleanup)
-+
-+ return {
-+ 'instance': inst,
-+ 'backends': backends,
-+ 'foo_backend': foo_backend,
-+ 'ou_people': ou_people,
-+ 'subsuffix_dn': subsuffix_dn,
-+ 'parent_suffix_dn': parent_suffix_dn,
-+ }
-+
-+
-+def test_subsuffix_with_entry_parent_in_tree(topo, setup_subsuffix_with_entry_parent):
-+ """Test that a sub-suffix with parent pointing to an entry is visible in the tree.
-+
-+ :id: 256f36f5-76ad-4043-ad8d-1f9e2afc4e1d
-+ :setup: Standalone instance with sub-suffix whose parent is an entry
-+ :steps:
-+ 1. Verify the sub-suffix backend exists
-+ 2. Get sub-suffixes of the root backend
-+ 3. Verify the sub-suffix appears in the list
-+ :expectedresults:
-+ 1. Backend should exist
-+ 2. Sub-suffixes should be retrievable
-+ 3. Sub-suffix should be visible (this is where the bug manifested)
-+ """
-+ backends = setup_subsuffix_with_entry_parent['backends']
-+ foo_backend = setup_subsuffix_with_entry_parent['foo_backend']
-+ subsuffix_dn = setup_subsuffix_with_entry_parent['subsuffix_dn']
-+
-+ # Step 1: Verify the sub-suffix backend exists
-+ assert foo_backend.exists(), "The foo backend should exist"
-+
-+ # Step 2: Get sub-suffixes of the root backend
-+ root_backend = backends.get(DEFAULT_SUFFIX)
-+ sub_suffixes = root_backend.get_sub_suffixes()
-+ log.info(f"Sub-suffixes found: {[s.get_attr_val_utf8('nsslapd-suffix') for s in sub_suffixes]}")
-+
-+ # Step 3: Verify sub-suffix is in the list
-+ sub_suffix_found = any(
-+ s.get_attr_val_utf8_l('nsslapd-suffix') == subsuffix_dn.lower()
-+ for s in sub_suffixes
-+ )
-+
-+ assert sub_suffix_found, (
-+ f"Sub-suffix {subsuffix_dn} should be visible in get_sub_suffixes(). "
-+ "The parent-suffix points to an entry, not a backend suffix."
-+ )
-+
-+
-+def test_subsuffix_in_backend_list(topo, setup_subsuffix_with_entry_parent):
-+ """Test that the sub-suffix appears in the backend list.
-+
-+ :id: 0ccc49af-91bb-4e8f-b0e1-1bd0b75c041b
-+ :setup: Standalone instance with sub-suffix configuration
-+ :steps:
-+ 1. Get all backends
-+ 2. Verify both root suffix and sub-suffix are present
-+ :expectedresults:
-+ 1. Should retrieve all backends
-+ 2. Both suffixes should be listed
-+ """
-+ backends = setup_subsuffix_with_entry_parent['backends']
-+ subsuffix_dn = setup_subsuffix_with_entry_parent['subsuffix_dn']
-+
-+ be_list = backends.list()
-+ suffixes = [be.get_attr_val_utf8_l('nsslapd-suffix') for be in be_list]
-+
-+ assert DEFAULT_SUFFIX.lower() in suffixes, \
-+ f"Root suffix {DEFAULT_SUFFIX} should be in the list"
-+ assert subsuffix_dn.lower() in suffixes, \
-+ f"Sub-suffix {subsuffix_dn} should be in the list"
-+
-+
-+def test_subsuffix_dn_boundary_matching():
-+ """Test that suffix matching respects DN component boundaries.
-+
-+ :id: 0b856e26-c394-4c36-b9ba-d7894aa2ed11
-+ :setup: None (unit test)
-+ :steps:
-+ 1. Test exact suffix match
-+ 2. Test proper DN ancestor match (ends with ,suffix)
-+ 3. Test that partial string matches are rejected
-+ :expectedresults:
-+ 1. Exact match should return True
-+ 2. Proper ancestor should return True
-+ 3. Partial string match should return False
-+ """
-+ from lib389.backend import is_subsuffix_of
-+
-+ all_suffixes = {'dc=com', 'dc=example,dc=com', 'ou=dept,dc=example,dc=com'}
-+
-+ # Test 1: Exact match
-+ assert is_subsuffix_of('dc=example,dc=com', 'dc=example,dc=com', all_suffixes), \
-+ "Exact match should return True"
-+
-+ # Test 2: Parent is an entry under the suffix (not itself a suffix)
-+ assert is_subsuffix_of('ou=people,dc=example,dc=com', 'dc=example,dc=com', all_suffixes), \
-+ "Parent entry under suffix should return True"
-+
-+ # Test 3: Parent IS a suffix - should return False (handled separately)
-+ assert not is_subsuffix_of('ou=dept,dc=example,dc=com', 'dc=example,dc=com', all_suffixes), \
-+ "Parent that is itself a suffix should return False"
-+
-+ # Test 4: Edge case - wrong DN boundary (string ends with suffix but wrong boundary)
-+ edge_suffixes = {'dc=com', 'st,dc=com'}
-+ assert is_subsuffix_of('dc=test,dc=com', 'dc=com', edge_suffixes), \
-+ "dc=test,dc=com should match dc=com"
-+ assert not is_subsuffix_of('dc=test,dc=com', 'st,dc=com', edge_suffixes), \
-+ "dc=test,dc=com should NOT match st,dc=com (wrong DN boundary)"
-+
-+ # Test 5: None input
-+ assert not is_subsuffix_of(None, 'dc=com', all_suffixes), \
-+ "None parent should return False"
-+
-+ # Test 6: Closest ancestor - should only match the nearest suffix
-+ # Hierarchy: dc=com -> dc=example,dc=com -> ou=branch,dc=example,dc=com (suffix)
-+ # -> ou=dept,ou=branch,dc=example,dc=com (entry) -> subsuffix
-+ # The subsuffix should only appear under ou=branch, not under dc=example,dc=com
-+ nested_suffixes = {'dc=com', 'dc=example,dc=com', 'ou=branch,dc=example,dc=com'}
-+ entry_parent = 'ou=dept,ou=branch,dc=example,dc=com'
-+ # Should match ou=branch (closest)
-+ assert is_subsuffix_of(entry_parent, 'ou=branch,dc=example,dc=com', nested_suffixes), \
-+ "Should match closest ancestor suffix (ou=branch)"
-+ # Should NOT match dc=example,dc=com (not closest)
-+ assert not is_subsuffix_of(entry_parent, 'dc=example,dc=com', nested_suffixes), \
-+ "Should NOT match distant ancestor (dc=example) - ou=branch is closer"
-+ # Should NOT match dc=com (not closest)
-+ assert not is_subsuffix_of(entry_parent, 'dc=com', nested_suffixes), \
-+ "Should NOT match distant ancestor (dc=com) - ou=branch is closer"
-+
-+ log.info("All DN boundary edge cases passed")
-+
-+
-+def test_deep_suffix_hierarchy(topo, request):
-+ """Test complex hierarchy: suffix -> suffix -> entry -> suffix -> suffix.
-+
-+ :id: fd06491a-defa-4780-8472-78c077febdfb
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create sub-suffix ou=branch (parent=dc=example,dc=com - a suffix)
-+ 2. Create entry ou=dept,ou=branch (not a suffix)
-+ 3. Create sub-suffix ou=team,ou=dept,ou=branch (parent=ou=dept - an entry)
-+ 4. Create sub-suffix ou=sub,ou=team,ou=dept,ou=branch (parent=ou=team - a suffix)
-+ 5. Verify all sub-suffixes are correctly placed in the tree
-+ :expectedresults:
-+ 1. Sub-suffix created successfully
-+ 2. Entry created successfully
-+ 3. Sub-suffix with entry parent created successfully
-+ 4. Sub-suffix with suffix parent created successfully
-+ 5. Tree hierarchy is correct
-+ """
-+ inst = topo.standalone
-+ backends = Backends(inst)
-+
-+ # Define the hierarchy
-+ branch_suffix = f'ou=branch,{DEFAULT_SUFFIX}'
-+ dept_entry = f'ou=dept,{branch_suffix}' # This is an ENTRY, not a suffix
-+ team_suffix = f'ou=team,{dept_entry}'
-+ sub_suffix = f'ou=sub,{team_suffix}'
-+
-+ created_backends = []
-+ created_entries = []
-+
-+ def cleanup():
-+ log.info("Cleaning up deep hierarchy test")
-+ for entry in reversed(created_entries):
-+ try:
-+ entry.delete()
-+ except Exception as e:
-+ log.warning(f"Failed to delete entry: {e}")
-+ for be in reversed(created_backends):
-+ try:
-+ be.delete()
-+ except Exception as e:
-+ log.warning(f"Failed to delete backend: {e}")
-+
-+ request.addfinalizer(cleanup)
-+
-+ # Step 1: Create ou=branch sub-suffix (parent is root suffix)
-+ log.info(f"Creating sub-suffix {branch_suffix}")
-+ branch_be = backends.create(properties={
-+ 'cn': 'branch',
-+ 'nsslapd-suffix': branch_suffix,
-+ 'parent': DEFAULT_SUFFIX,
-+ })
-+ created_backends.append(branch_be)
-+ branch_ous = OrganizationalUnits(inst, DEFAULT_SUFFIX)
-+ branch_ou = branch_ous.create(properties={'ou': 'branch'})
-+ created_entries.append(branch_ou)
-+
-+ # Step 2: Create ou=dept entry under branch (NOT a suffix)
-+ log.info(f"Creating entry {dept_entry}")
-+ dept_ous = OrganizationalUnits(inst, branch_suffix)
-+ dept_ou = dept_ous.create(properties={'ou': 'dept'})
-+ created_entries.append(dept_ou)
-+
-+ # Step 3: Create ou=team sub-suffix (parent is dept ENTRY, not a suffix)
-+ log.info(f"Creating sub-suffix {team_suffix} with entry parent {dept_entry}")
-+ team_be = backends.create(properties={
-+ 'cn': 'team',
-+ 'nsslapd-suffix': team_suffix,
-+ 'parent': dept_entry, # Parent is an ENTRY!
-+ })
-+ created_backends.append(team_be)
-+ team_ous = OrganizationalUnits(inst, dept_entry)
-+ team_ou = team_ous.create(properties={'ou': 'team'})
-+ created_entries.append(team_ou)
-+
-+ # Step 4: Create ou=sub sub-suffix (parent is team suffix)
-+ log.info(f"Creating sub-suffix {sub_suffix} with suffix parent {team_suffix}")
-+ sub_be = backends.create(properties={
-+ 'cn': 'sub',
-+ 'nsslapd-suffix': sub_suffix,
-+ 'parent': team_suffix, # Parent is a SUFFIX
-+ })
-+ created_backends.append(sub_be)
-+ sub_ous = OrganizationalUnits(inst, team_suffix)
-+ sub_ou = sub_ous.create(properties={'ou': 'sub'})
-+ created_entries.append(sub_ou)
-+
-+ # Step 5: Verify the tree hierarchy
-+ log.info("Verifying tree hierarchy...")
-+
-+ # Root should have branch as sub-suffix
-+ root_be = backends.get(DEFAULT_SUFFIX)
-+ root_subs = root_be.get_sub_suffixes()
-+ root_sub_suffixes = [s.get_attr_val_utf8_l('nsslapd-suffix') for s in root_subs]
-+ log.info(f"Root sub-suffixes: {root_sub_suffixes}")
-+ assert branch_suffix.lower() in root_sub_suffixes, \
-+ f"branch should be under root suffix"
-+
-+ # Branch should have team as sub-suffix (even though team's parent is an entry)
-+ branch_be_obj = backends.get(branch_suffix)
-+ branch_subs = branch_be_obj.get_sub_suffixes()
-+ branch_sub_suffixes = [s.get_attr_val_utf8_l('nsslapd-suffix') for s in branch_subs]
-+ log.info(f"Branch sub-suffixes: {branch_sub_suffixes}")
-+ assert team_suffix.lower() in branch_sub_suffixes, \
-+ f"team should be under branch suffix (parent is entry under branch)"
-+
-+ # Team should have sub as sub-suffix
-+ team_be_obj = backends.get(team_suffix)
-+ team_subs = team_be_obj.get_sub_suffixes()
-+ team_sub_suffixes = [s.get_attr_val_utf8_l('nsslapd-suffix') for s in team_subs]
-+ log.info(f"Team sub-suffixes: {team_sub_suffixes}")
-+ assert sub_suffix.lower() in team_sub_suffixes, \
-+ f"sub should be under team suffix"
-+
-+ log.info("Deep hierarchy test passed!")
-+
-+
-+if __name__ == '__main__':
-+ CURRENT_FILE = os.path.realpath(__file__)
-+ pytest.main(["-s", CURRENT_FILE])
-diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
-index db464b43a..274d45abe 100644
---- a/src/lib389/lib389/backend.py
-+++ b/src/lib389/lib389/backend.py
-@@ -38,6 +38,36 @@ from lib389.lint import DSBLE0001, DSBLE0002, DSBLE0003, DSBLE0004, DSBLE0005, D
- from lib389.plugins import USNPlugin
-
-
-+def is_subsuffix_of(sub_parent, be_suffix, all_suffixes):
-+ """Check if sub_parent indicates this is a sub-suffix of be_suffix.
-+
-+ Returns True only if be_suffix is the CLOSEST ancestor suffix of sub_parent.
-+ This prevents a sub-suffix from appearing under multiple ancestors.
-+
-+ :param sub_parent: The nsslapd-parent-suffix value (lowercase)
-+ :param be_suffix: The suffix to check against (lowercase)
-+ :param all_suffixes: Set of all backend suffixes (lowercase)
-+ :returns: True if be_suffix is the closest ancestor suffix
-+ """
-+ if not sub_parent:
-+ return False
-+ if sub_parent == be_suffix:
-+ return True
-+ if sub_parent in all_suffixes:
-+ # sub_parent is itself a suffix, will be handled separately
-+ return False
-+ if not sub_parent.endswith(',' + be_suffix):
-+ return False
-+ # Find the closest (longest) matching suffix for this parent
-+ best_match = None
-+ for sfx in all_suffixes:
-+ if sub_parent == sfx or sub_parent.endswith(',' + sfx):
-+ if best_match is None or len(sfx) > len(best_match):
-+ best_match = sfx
-+ # Only return True if be_suffix is the closest match
-+ return best_match == be_suffix
-+
-+
- class BackendLegacy(object):
- proxied_methods = 'search_s getEntry'.split()
-
-@@ -1104,22 +1134,27 @@ class Backend(DSLdapObject):
- vlv.create(rdn="cn=" + vlvname, properties=props, basedn=basedn)
-
- def get_sub_suffixes(self):
-- """Return a list of Backend's
-- returns: a List of subsuffix entries
-+ """Return a list of Backend's that are sub-suffixes of this backend.
-+ :returns: A list of Backend instances that are sub-suffixes
- """
- subsuffixes = []
- top_be_suffix = self.get_attr_val_utf8_l('nsslapd-suffix')
-+ if not top_be_suffix:
-+ return subsuffixes
-+
- mts = self._mts.list()
-+ be_insts = Backends(self._instance).list()
-+ all_suffixes = {be.get_attr_val_utf8_l('nsslapd-suffix') for be in be_insts}
-+
- for mt in mts:
- parent_suffix = mt.get_attr_val_utf8_l('nsslapd-parent-suffix')
- if parent_suffix is None:
- continue
-- if parent_suffix == top_be_suffix:
-+
-+ if is_subsuffix_of(parent_suffix, top_be_suffix, all_suffixes):
- child_suffix = mt.get_attr_val_utf8_l('cn')
-- be_insts = Backends(self._instance).list()
- for be in be_insts:
-- be_suffix = be.get_attr_val_utf8_l('nsslapd-suffix')
-- if child_suffix == be_suffix:
-+ if child_suffix == be.get_attr_val_utf8_l('nsslapd-suffix'):
- subsuffixes.append(be)
- break
- return subsuffixes
-diff --git a/src/lib389/lib389/cli_conf/backend.py b/src/lib389/lib389/cli_conf/backend.py
-index d0ec4bd9e..9772e39d4 100644
---- a/src/lib389/lib389/cli_conf/backend.py
-+++ b/src/lib389/lib389/cli_conf/backend.py
-@@ -7,7 +7,7 @@
- # See LICENSE for details.
- # --- END COPYRIGHT BLOCK ---
-
--from lib389.backend import Backend, Backends, DatabaseConfig, BackendSuffixView
-+from lib389.backend import Backend, Backends, DatabaseConfig, BackendSuffixView, is_subsuffix_of
- from lib389.configurations.sample import (
- create_base_domain,
- create_base_org,
-@@ -338,6 +338,7 @@ def is_db_replicated(inst, suffix):
- def backend_get_subsuffixes(inst, basedn, log, args):
- subsuffixes = []
- be_insts = MANY(inst).list()
-+ all_suffixes = {be.get_attr_val_utf8_l('nsslapd-suffix') for be in be_insts}
- for be in be_insts:
- be_suffix = be.get_attr_val_utf8_l('nsslapd-suffix')
- if be_suffix == args.be_name.lower():
-@@ -347,7 +348,7 @@ def backend_get_subsuffixes(inst, basedn, log, args):
- db_type = "suffix"
- sub = mt.get_attr_val_utf8_l('nsslapd-parent-suffix')
- sub_be = mt.get_attr_val_utf8_l('nsslapd-backend')
-- if sub == be_suffix:
-+ if is_subsuffix_of(sub, be_suffix, all_suffixes):
- # We have a subsuffix (maybe a db link?)
- if is_db_link(inst, sub_be):
- db_type = "link"
-@@ -399,38 +400,34 @@ def build_node(suffix, be_name, subsuf=False, link=False, replicated=False):
- }
-
-
--def backend_build_tree(inst, be_insts, nodes):
-- """Recursively build the tree
-- """
-- if len(nodes) == 0:
-- # Done
-+def backend_build_tree(inst, be_insts, nodes, all_suffixes):
-+ """Recursively build the tree."""
-+ if not nodes:
- return
-
- for node in nodes:
-- node_suffix = node['id']
-+ node_suffix = node['id'].lower()
- # Get sub suffixes and chaining of node
- for be in be_insts:
- be_suffix = be.get_attr_val_utf8_l('nsslapd-suffix')
-- if be_suffix == node_suffix.lower():
-+ if be_suffix == node_suffix:
- # We have our parent, now find the children
- mts = be._mts.list()
--
- for mt in mts:
- sub_parent = mt.get_attr_val_utf8_l('nsslapd-parent-suffix')
- sub_be = mt.get_attr_val_utf8_l('nsslapd-backend')
- sub_suffix = mt.get_attr_val_utf8_l('cn')
-- if sub_parent == be_suffix:
-+ if is_subsuffix_of(sub_parent, be_suffix, all_suffixes):
- # We have a subsuffix (maybe a db link?)
- link = is_db_link(inst, sub_be)
- replicated = is_db_replicated(inst, sub_suffix)
- node['children'].append(build_node(sub_suffix,
-- sub_be,
-- subsuf=True,
-- link=link,
-- replicated=replicated))
--
-+ sub_be,
-+ subsuf=True,
-+ link=link,
-+ replicated=replicated))
- # Recurse over the new subsuffixes
-- backend_build_tree(inst, be_insts, node['children'])
-+ backend_build_tree(inst, be_insts, node['children'], all_suffixes)
- break
-
-
-@@ -471,7 +468,8 @@ def backend_get_tree(inst, basedn, log, args):
- else:
- # Build the tree
- be_insts = Backends(inst).list()
-- backend_build_tree(inst, be_insts, nodes)
-+ all_suffixes = {be.get_attr_val_utf8_l('nsslapd-suffix') for be in be_insts}
-+ backend_build_tree(inst, be_insts, nodes, all_suffixes)
-
- # Done
- if args.json:
---
-2.52.0
-
diff --git a/0013-Issue-7184-argparse.HelpFormatter-_format_actions_us.patch b/0013-Issue-7184-argparse.HelpFormatter-_format_actions_us.patch
deleted file mode 100644
index 2acbcf4..0000000
--- a/0013-Issue-7184-argparse.HelpFormatter-_format_actions_us.patch
+++ /dev/null
@@ -1,48 +0,0 @@
-From 98229fe9cde11aede104dfc669274e4e3745b4e8 Mon Sep 17 00:00:00 2001
-From: Mark Reynolds <mreynolds@redhat.com>
-Date: Mon, 12 Jan 2026 13:53:05 -0500
-Subject: [PATCH] Issue 7184 - argparse.HelpFormatter _format_actions_usage()
- is deprecated
-
-Description:
-
-_format_actions_usage() was removed in python 3.15. Instead we can use
-_get_actions_usage_parts() but it also behaves differently between
-python 3.14 and 3.15 so we need special handling.
-
-Relates: https://github.com/389ds/389-ds-base/issues/7184
-
-Reviewed by: spichugi(Thanks!)
----
- src/lib389/lib389/cli_base/__init__.py | 15 ++++++++++++++-
- 1 file changed, 14 insertions(+), 1 deletion(-)
-
-diff --git a/src/lib389/lib389/cli_base/__init__.py b/src/lib389/lib389/cli_base/__init__.py
-index 06b8f9964..f1055aadc 100644
---- a/src/lib389/lib389/cli_base/__init__.py
-+++ b/src/lib389/lib389/cli_base/__init__.py
-@@ -413,7 +413,20 @@ class CustomHelpFormatter(argparse.HelpFormatter):
-
- def _format_usage(self, usage, actions, groups, prefix):
- usage = super(CustomHelpFormatter, self)._format_usage(usage, actions, groups, prefix)
-- formatted_options = self._format_actions_usage(parent_arguments, [])
-+
-+ if sys.version_info < (3, 13):
-+ # Use _format_actions_usage() for Python 3.12 and earlier
-+ formatted_options = self._format_actions_usage(parent_arguments, [])
-+ else:
-+ # Use _get_actions_usage_parts() for Python 3.13 and later
-+ action_parts = self._get_actions_usage_parts(parent_arguments, [])
-+ if sys.version_info >= (3, 15):
-+ # Python 3.15 returns a tuple (list of actions, count of actions)
-+ formatted_options = ' '.join(action_parts[0])
-+ else:
-+ # Python 3.13 and 3.14 return a list of actions
-+ formatted_options = ' '.join(action_parts)
-+
- # If formatted_options already in usage - remove them
- if formatted_options in usage:
- usage = usage.replace(f' {formatted_options}', '')
---
-2.52.0
-
diff --git a/0014-Issue-7027-2nd-389-ds-base-OpenScanHub-Leaks-Detecte.patch b/0014-Issue-7027-2nd-389-ds-base-OpenScanHub-Leaks-Detecte.patch
deleted file mode 100644
index 05b2c85..0000000
--- a/0014-Issue-7027-2nd-389-ds-base-OpenScanHub-Leaks-Detecte.patch
+++ /dev/null
@@ -1,53 +0,0 @@
-From b5c8dfe16456fbd9d360cec492f869f42f401053 Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Fri, 30 Jan 2026 12:00:13 +0100
-Subject: [PATCH] Issue 7027 - (2nd) 389-ds-base OpenScanHub Leaks Detected
- (#7211)
-
-Fix Description:
-Update coverity annotations.
-
-Relates: https://github.com/389ds/389-ds-base/issues/7027
-
-Reviewed by: @aadhikar (Thanks!)
----
- ldap/servers/slapd/log.c | 6 +++---
- 1 file changed, 3 insertions(+), 3 deletions(-)
-
-diff --git a/ldap/servers/slapd/log.c b/ldap/servers/slapd/log.c
-index ea744ac1e..80c07382a 100644
---- a/ldap/servers/slapd/log.c
-+++ b/ldap/servers/slapd/log.c
-@@ -206,8 +206,8 @@ compress_log_file(char *log_name, int32_t mode)
-
- if ((source = fopen(log_name, "r")) == NULL) {
- /* Failed to open log file */
-- /* coverity[leaked_storage] gzclose does close FD */
- gzclose(outfile);
-+ /* coverity[leaked_handle] gzclose does close FD */
- return -1;
- }
-
-@@ -217,17 +217,17 @@ compress_log_file(char *log_name, int32_t mode)
- if (bytes_written == 0)
- {
- fclose(source);
-- /* coverity[leaked_storage] gzclose does close FD */
- gzclose(outfile);
-+ /* coverity[leaked_handle] gzclose does close FD */
- return -1;
- }
- bytes_read = fread(buf, 1, LOG_CHUNK, source);
- }
-- /* coverity[leaked_storage] gzclose does close FD */
- gzclose(outfile);
- fclose(source);
- PR_Delete(log_name); /* remove the old uncompressed log */
-
-+ /* coverity[leaked_handle] gzclose does close FD */
- return 0;
- }
-
---
-2.52.0
-
diff --git a/0015-Issue-7213-MDB_BAD_VALSIZE-error-while-handling-VLV-.patch b/0015-Issue-7213-MDB_BAD_VALSIZE-error-while-handling-VLV-.patch
deleted file mode 100644
index e0e21bb..0000000
--- a/0015-Issue-7213-MDB_BAD_VALSIZE-error-while-handling-VLV-.patch
+++ /dev/null
@@ -1,197 +0,0 @@
-From 46ce38c02d0f86725829592bbff27679a625e9a0 Mon Sep 17 00:00:00 2001
-From: progier389 <progier@redhat.com>
-Date: Mon, 2 Feb 2026 15:39:18 +0100
-Subject: [PATCH] Issue 7213 - MDB_BAD_VALSIZE error while handling VLV (#7214)
-
-* Issue 7213 - MDB_BAD_VALSIZE error while handling VLV
-Avoid failing lmdb operation when handling VLV index by truncating the key so that key+data is small enough.
-
-Issue: #7213
-
-Reviewed by: @mreynolds389 , @vashirov (Thanks!)
-
-Assisted by: Claude A/I
-
-(cherry picked from commit 5ebce22d4214bec5ed94ad84c4448164be99389a)
----
- .../tests/suites/vlv/regression_test.py | 110 ++++++++++++++++++
- .../slapd/back-ldbm/db-mdb/mdb_layer.c | 5 +
- ldap/servers/slapd/back-ldbm/vlv.c | 7 +-
- 3 files changed, 121 insertions(+), 1 deletion(-)
-
-diff --git a/dirsrvtests/tests/suites/vlv/regression_test.py b/dirsrvtests/tests/suites/vlv/regression_test.py
-index f7847ac74..7cdf16a84 100644
---- a/dirsrvtests/tests/suites/vlv/regression_test.py
-+++ b/dirsrvtests/tests/suites/vlv/regression_test.py
-@@ -1175,6 +1175,116 @@ def test_vlv_with_mr(vlv_setup_with_uid_mr):
-
-
-
-+def test_vlv_long_attribute_value(topology_st, request):
-+ """
-+ Test VLV with an entry containing a very long attribute value (2K).
-+
-+ :id: 99126fa4-003e-11f1-b7d6-c85309d5c3e3
-+ :setup: Standalone instance.
-+ :steps:
-+ 1. Cleanup leftover from previous tests
-+ 2. Create VLV search and index on cn attribute
-+ 3. Reindex VLV
-+ 4. Add an entry with a cn attribute having 2K character value
-+ 5. Verify the entry was added successfully
-+ 6. Perform a VLV search to ensure it still works
-+ 7. Add another entry with a cn attribute having 2K character value
-+ 8. Verify the entry was added successfully
-+ 9. Perform a VLV search to ensure it still works
-+ :expectedresults:
-+ 1. Should Success.
-+ 2. Should Success.
-+ 3. Should Success.
-+ 4. Should Success.
-+ 5. Should Success.
-+ 6. Should Success.
-+ 7. Should Success.
-+ 8. Should Success.
-+ 9. Should Success.
-+ """
-+ inst = topology_st.standalone
-+ reindex_task = Tasks(inst)
-+
-+ users_to_delete = []
-+
-+ def fin():
-+ cleanup(inst)
-+ # Clean the added users
-+ for user in users_to_delete:
-+ user.delete()
-+
-+ if not DEBUGGING:
-+ request.addfinalizer(fin)
-+
-+ # Clean previous tests leftover
-+ fin()
-+
-+ # Create VLV search and index
-+ vlv_search, vlv_index = create_vlv_search_and_index(inst)
-+ assert reindex_task.reindex(
-+ suffix=DEFAULT_SUFFIX,
-+ attrname=vlv_index.rdn,
-+ args={TASK_WAIT: True},
-+ vlv=True
-+ ) == 0
-+
-+ # Add a few regular users first
-+ add_users(inst, 10)
-+
-+ # Create a very long cn value (2K characters)
-+ long_cn_value = 'a' * 2048 + '1'
-+
-+ # Add an entry with the long cn attribute
-+ users = UserAccounts(inst, DEFAULT_SUFFIX)
-+ user_properties = {
-+ 'uid': 'longcnuser1',
-+ 'cn': long_cn_value,
-+ 'sn': 'user1',
-+ 'uidNumber': '99999',
-+ 'gidNumber': '99999',
-+ 'homeDirectory': '/home/longcnuser1'
-+ }
-+ user = users.create(properties=user_properties)
-+ users_to_delete.append(user);
-+
-+ # Verify the entry was created and has the long cn value
-+ entry = user.get_attr_vals_utf8('cn')
-+ assert entry[0] == long_cn_value
-+ log.info(f'Successfully created user with cn length: {len(entry[0])}')
-+
-+ # Perform VLV search to ensure VLV still works with long attribute values
-+ conn = open_new_ldapi_conn(inst.serverid)
-+ count = len(conn.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(uid=*)"))
-+ assert count > 0
-+ log.info(f'VLV search successful with {count} entries including entry with 2K cn value')
-+
-+ # Add another entry with the long cn attribute
-+ long_cn_value = 'a' * 2048 + '2'
-+
-+ user_properties = {
-+ 'uid': 'longcnuser2',
-+ 'cn': long_cn_value,
-+ 'sn': 'user2',
-+ 'uidNumber': '99998',
-+ 'gidNumber': '99998',
-+ 'homeDirectory': '/home/longcnuser2'
-+ }
-+ user = users.create(properties=user_properties)
-+ users_to_delete.append(user);
-+
-+ # Verify the entry was created and has the long cn value
-+ entry = user.get_attr_vals_utf8('cn')
-+ assert entry[0] == long_cn_value
-+ log.info(f'Successfully created user with cn length: {len(entry[0])}')
-+
-+ # Perform VLV search to ensure VLV still works with long attribute values
-+ conn = open_new_ldapi_conn(inst.serverid)
-+ count = len(conn.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(uid=*)"))
-+ assert count > 1
-+ log.info(f'VLV search successful with {count} entries including entry with 2K cn value')
-+
-+
-+
- if __name__ == "__main__":
- # Run isolated
- # -s for DEBUG mode
-diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c
-index d320ecbeb..cd797621d 100644
---- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c
-+++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c
-@@ -2134,10 +2134,15 @@ void *dbmdb_recno_cache_build(void *arg)
- recno = 1;
- }
- while (rc == 0) {
-+ struct ldbminfo *li = (struct ldbminfo *)rcctx->cursor->be->be_database->plg_private;
- slapi_log_err(SLAPI_LOG_DEBUG, "dbmdb_recno_cache_build", "recno=%d\n", recno);
- if (recno % RECNO_CACHE_INTERVAL == 1) {
- /* Prepare the cache data */
- len = sizeof(*rce) + data.mv_size + key.mv_size;
-+ if (len > li->li_max_key_len) {
-+ key.mv_size = li->li_max_key_len - data.mv_size - sizeof(*rce);
-+ len = li->li_max_key_len;
-+ }
- rce = (dbmdb_recno_cache_elmt_t*)slapi_ch_malloc(len);
- rce->len = len;
- rce->recno = recno;
-diff --git a/ldap/servers/slapd/back-ldbm/vlv.c b/ldap/servers/slapd/back-ldbm/vlv.c
-index 8f9263f25..f2f882b5a 100644
---- a/ldap/servers/slapd/back-ldbm/vlv.c
-+++ b/ldap/servers/slapd/back-ldbm/vlv.c
-@@ -866,6 +866,7 @@ do_vlv_update_index(back_txn *txn, struct ldbminfo *li, Slapi_PBlock *pb, struct
- struct vlv_key *key = NULL;
- dbi_val_t data = {0};
- dblayer_private *priv = NULL;
-+ size_t key_size_limit = li->li_max_key_len - sizeof(entry->ep_id);
-
- slapi_pblock_get(pb, SLAPI_BACKEND, &be);
- priv = (dblayer_private *)li->li_dblayer_private;
-@@ -886,6 +887,10 @@ do_vlv_update_index(back_txn *txn, struct ldbminfo *li, Slapi_PBlock *pb, struct
- return rc;
- }
-
-+ /* Truncate the key if it is too long */
-+ if (key->key.size > key_size_limit) {
-+ key->key.size = key_size_limit;
-+ }
- if (NULL != txn) {
- db_txn = txn->back_txn_txn;
- } else {
-@@ -930,7 +935,7 @@ do_vlv_update_index(back_txn *txn, struct ldbminfo *li, Slapi_PBlock *pb, struct
- if (txn && txn->back_special_handling_fn) {
- rc = txn->back_special_handling_fn(be, BTXNACT_VLV_DEL, db, &key->key, &data, txn);
- } else {
-- rc = dblayer_db_op(be, db, db_txn, DBI_OP_DEL, &key->key, NULL);
-+ rc = dblayer_db_op(be, db, db_txn, DBI_OP_DEL, &key->key, &data);
- }
- if (rc == 0) {
- if (txn && txn->back_special_handling_fn) {
---
-2.52.0
-
diff --git a/0016-Issue-7194-Repl-Log-Analysis-Add-CSN-propagation-det.patch b/0016-Issue-7194-Repl-Log-Analysis-Add-CSN-propagation-det.patch
deleted file mode 100644
index 900f389..0000000
--- a/0016-Issue-7194-Repl-Log-Analysis-Add-CSN-propagation-det.patch
+++ /dev/null
@@ -1,1645 +0,0 @@
-From 64845ffd989313f9629bc1afe1052cfa0eef00b1 Mon Sep 17 00:00:00 2001
-From: Simon Pichugin <spichugi@redhat.com>
-Date: Tue, 3 Feb 2026 17:17:02 -0800
-Subject: [PATCH] Issue 7194 - Repl Log Analysis - Add CSN propagation details
- (#7195)
-MIME-Version: 1.0
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 8bit
-
-Description: The replication log analyzer now shows per‑CSN propagation
-details and the console UI can drill into them from chart points. This
-adds CSN IDs to chart datapoints, builds detailed arrivals/hops data, and
-links replica IDs to origin servers for more accurate origin detection.
-
-The report JSON now includes csnDetails and sampling metadata; when
-sampling is active, CSN details are limited to sampled IDs to control
-memory use. A new originIncludedInArrivals flag is exposed and the UI
-shows an explicit note when origin records are outside the time range.
-The cockpit report modal gains an interactive CSN detail view and
-clickable chart points.
-
-Tests were expanded to cover CSN details, origin out‑of‑scope behavior,
-and partial replication, and include helper functions to reduce duplication.
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7194
-
-Reviewed by: @progier389, @mreynolds389 (Thanks!!)
----
- .../replication/repl_log_monitoring_test.py | 481 +++++++++++++++--
- .../src/lib/monitor/monitorModals.jsx | 484 +++++++++++++++++-
- src/lib389/lib389/repltools.py | 270 +++++++++-
- 3 files changed, 1166 insertions(+), 69 deletions(-)
-
-diff --git a/dirsrvtests/tests/suites/replication/repl_log_monitoring_test.py b/dirsrvtests/tests/suites/replication/repl_log_monitoring_test.py
-index 665fcb96f..855005ff9 100644
---- a/dirsrvtests/tests/suites/replication/repl_log_monitoring_test.py
-+++ b/dirsrvtests/tests/suites/replication/repl_log_monitoring_test.py
-@@ -21,7 +21,7 @@ from lib389.backend import Backends
- from lib389.topologies import topology_m4 as topo_m4
- from lib389.idm.user import UserAccount
- from lib389.replica import ReplicationManager
--from lib389.repltools import ReplicationLogAnalyzer
-+from lib389.repltools import ReplicationLogAnalyzer, DSLogParser
- from lib389._constants import *
-
- pytestmark = pytest.mark.tier0
-@@ -105,6 +105,101 @@ def _cleanup_multi_suffix_test(test_users_by_suffix, tmp_dir, suppliers, extra_s
- log.error(f"Error cleaning up temporary directory: {e}")
-
-
-+def _clear_access_logs(suppliers):
-+ """Clear access logs for all suppliers and restart."""
-+ for supplier in suppliers:
-+ supplier.deleteAccessLogs(restart=True)
-+
-+
-+def _restart_suppliers(suppliers):
-+ """Restart all suppliers."""
-+ for supplier in suppliers:
-+ supplier.restart()
-+
-+
-+def _get_log_dirs(suppliers):
-+ """Return log directories for all suppliers."""
-+ return [s.ds_paths.log_dir for s in suppliers]
-+
-+
-+def _load_json(path):
-+ """Load and return JSON from file."""
-+ with open(path, 'r') as f:
-+ return json.load(f)
-+
-+
-+def _pause_agreements(supplier, suffix):
-+ """Pause outbound agreements and return list of paused tuples."""
-+ paused = []
-+ for agmt in supplier.agreement.list(suffix=suffix):
-+ supplier.agreement.pause(agmt.dn)
-+ paused.append((supplier, agmt.dn))
-+ return paused
-+
-+
-+def _resume_agreements(paused_agreements):
-+ """Resume paused replication agreements."""
-+ for supplier_obj, dn in paused_agreements:
-+ try:
-+ supplier_obj.agreement.resume(dn)
-+ except Exception as e:
-+ log.warning(f"Failed to resume agreement {dn}: {e}")
-+
-+
-+def _assert_csn_details_schema(json_data):
-+ """Validate csnDetails presence and basic structure."""
-+ assert 'csnDetails' in json_data, "Expected csnDetails in JSON output for drill-down"
-+ csn_details = json_data['csnDetails']
-+ if csn_details:
-+ # Check structure of at least one CSN detail entry
-+ first_csn = next(iter(csn_details.values()))
-+ assert 'csn' in first_csn, "CSN detail should contain 'csn' field"
-+ assert 'targetDn' in first_csn, "CSN detail should contain 'targetDn' field"
-+ assert 'suffix' in first_csn, "CSN detail should contain 'suffix' field"
-+ assert 'globalLag' in first_csn, "CSN detail should contain 'globalLag' field"
-+ assert 'originServer' in first_csn, "CSN detail should contain 'originServer' field"
-+ assert 'arrivals' in first_csn, "CSN detail should contain 'arrivals' list"
-+ assert 'hops' in first_csn, "CSN detail should contain 'hops' list"
-+ assert isinstance(first_csn['arrivals'], list), "arrivals should be a list"
-+
-+ # Verify arrivals structure
-+ if first_csn['arrivals']:
-+ first_arrival = first_csn['arrivals'][0]
-+ assert 'server' in first_arrival, "Arrival should contain 'server' field"
-+ assert 'timestamp' in first_arrival, "Arrival should contain 'timestamp' field"
-+ assert 'relativeDelay' in first_arrival, "Arrival should contain 'relativeDelay' field"
-+
-+ # Verify csnId is included in datapoints for cross-reference
-+ if 'replicationLags' in json_data and json_data['replicationLags'].get('series'):
-+ for series in json_data['replicationLags']['series']:
-+ for datapoint in series['datapoints']:
-+ assert 'csnId' in datapoint, "Datapoint should contain 'csnId' for drill-down"
-+
-+ return csn_details
-+
-+
-+def _find_latest_logtime_for_prefix(log_dir, suffix, start_time, end_time, user_prefix):
-+ latest = None
-+ for fname in os.listdir(log_dir):
-+ if not fname.startswith('access'):
-+ continue
-+ full_path = os.path.join(log_dir, fname)
-+ parser = DSLogParser(
-+ logname=full_path,
-+ suffixes=[suffix],
-+ tz=timezone.utc,
-+ start_time=start_time,
-+ end_time=end_time
-+ )
-+ for record in parser.parse_file():
-+ target_dn = record.get('target_dn') or ''
-+ if user_prefix in target_dn:
-+ ts = record.get('timestamp')
-+ if ts and (latest is None or ts > latest):
-+ latest = ts
-+ return latest
-+
-+
- def test_replication_log_monitoring_basic(topo_m4):
- """Test basic replication log monitoring functionality
-
-@@ -128,8 +223,7 @@ def test_replication_log_monitoring_basic(topo_m4):
-
- try:
- # Clear logs and restart servers
-- for supplier in suppliers:
-- supplier.deleteAccessLogs(restart=True)
-+ _clear_access_logs(suppliers)
-
- # Generate test data with known patterns
- log.info('Creating test data...')
-@@ -140,11 +234,10 @@ def test_replication_log_monitoring_basic(topo_m4):
- repl.test_replication_topology(topo_m4)
-
- # Restart to flush logs
-- for supplier in suppliers:
-- supplier.restart()
-+ _restart_suppliers(suppliers)
-
- # Configure monitoring
-- log_dirs = [s.ds_paths.log_dir for s in suppliers]
-+ log_dirs = _get_log_dirs(suppliers)
- repl_monitor = ReplicationLogAnalyzer(
- log_dirs=log_dirs,
- suffixes=[DEFAULT_SUFFIX],
-@@ -177,23 +270,23 @@ def test_replication_log_monitoring_basic(topo_m4):
- assert DEFAULT_SUFFIX in csv_content
-
- # Verify PatternFly JSON content
-- with open(generated_files['json'], 'r') as f:
-- json_data = json.load(f)
-- assert 'replicationLags' in json_data
-- assert json_data['replicationLags']['series'], "Expected replication lag series in JSON output"
-+ json_data = _load_json(generated_files['json'])
-+ assert 'replicationLags' in json_data
-+ assert json_data['replicationLags']['series'], "Expected replication lag series in JSON output"
-+
-+ _assert_csn_details_schema(json_data)
-
- # Verify JSON summary
-- with open(generated_files['summary'], 'r') as f:
-- summary = json.load(f)
-- assert 'analysis_summary' in summary
-- stats = summary['analysis_summary']
-+ summary = _load_json(generated_files['summary'])
-+ assert 'analysis_summary' in summary
-+ stats = summary['analysis_summary']
-
-- # Verify basic stats
-- assert stats['total_servers'] == len(suppliers)
-- assert stats['total_updates'] > 0
-- assert stats['updates_by_suffix'][DEFAULT_SUFFIX] > 0
-- assert 'average_lag' in stats
-- assert 'maximum_lag' in stats
-+ # Verify basic stats
-+ assert stats['total_servers'] == len(suppliers)
-+ assert stats['total_updates'] > 0
-+ assert stats['updates_by_suffix'][DEFAULT_SUFFIX] > 0
-+ assert 'average_lag' in stats
-+ assert 'maximum_lag' in stats
-
- finally:
- _cleanup_test_data(test_users, tmp_dir)
-@@ -221,8 +314,7 @@ def test_replication_log_monitoring_advanced(topo_m4):
-
- try:
- # Clear logs and restart servers
-- for supplier in suppliers:
-- supplier.deleteAccessLogs(restart=True)
-+ _clear_access_logs(suppliers)
-
- # Generate test data
- start_time = datetime.now(timezone.utc)
-@@ -240,10 +332,9 @@ def test_replication_log_monitoring_advanced(topo_m4):
- end_time = datetime.now(timezone.utc)
-
- # Restart to flush logs
-- for supplier in suppliers:
-- supplier.restart()
-+ _restart_suppliers(suppliers)
-
-- log_dirs = [s.ds_paths.log_dir for s in suppliers]
-+ log_dirs = _get_log_dirs(suppliers)
-
- # Test 1: Lag time filtering
- repl_monitor = ReplicationLogAnalyzer(
-@@ -374,8 +465,7 @@ def test_replication_log_monitoring_multi_suffix(topo_m4):
- repl = ReplicationManager(suffix)
- repl.test_replication_topology(topo_m4)
-
-- for supplier in suppliers:
-- supplier.deleteAccessLogs(restart=True)
-+ _clear_access_logs(suppliers)
-
- start_time = datetime.now(timezone.utc)
-
-@@ -400,11 +490,10 @@ def test_replication_log_monitoring_multi_suffix(topo_m4):
- end_time = datetime.now(timezone.utc)
-
- # Restart to flush logs
-- for supplier in suppliers:
-- supplier.restart()
-+ _restart_suppliers(suppliers)
-
- # Monitor all suffixes
-- log_dirs = [s.ds_paths.log_dir for s in suppliers]
-+ log_dirs = _get_log_dirs(suppliers)
- repl_monitor = ReplicationLogAnalyzer(
- log_dirs=log_dirs,
- suffixes=all_suffixes,
-@@ -466,8 +555,7 @@ def test_replication_log_monitoring_filter_combinations(topo_m4):
-
- try:
- # Clear logs and restart servers
-- for supplier in suppliers:
-- supplier.deleteAccessLogs(restart=True)
-+ _clear_access_logs(suppliers)
-
- # Generate varied test data
- start_time = datetime.now(timezone.utc)
-@@ -475,9 +563,7 @@ def test_replication_log_monitoring_filter_combinations(topo_m4):
-
- # Create different lag patterns
- # Pause outbound agreements from supplier1 to build a replication backlog
-- for agmt in suppliers[0].agreement.list(suffix=DEFAULT_SUFFIX):
-- suppliers[0].agreement.pause(agmt.dn)
-- paused_agreements.append((suppliers[0], agmt.dn))
-+ paused_agreements = _pause_agreements(suppliers[0], DEFAULT_SUFFIX)
-
- for i, user in enumerate(test_users):
- if i % 3 == 0:
-@@ -502,10 +588,9 @@ def test_replication_log_monitoring_filter_combinations(topo_m4):
- end_time = datetime.now(timezone.utc)
-
- # Restart to flush logs
-- for supplier in suppliers:
-- supplier.restart()
-+ _restart_suppliers(suppliers)
-
-- log_dirs = [s.ds_paths.log_dir for s in suppliers]
-+ log_dirs = _get_log_dirs(suppliers)
-
- # Test combined filters
- lag_threshold = 0.5
-@@ -543,11 +628,321 @@ def test_replication_log_monitoring_filter_combinations(topo_m4):
- dt = datetime.fromtimestamp(t, timezone.utc)
- assert start_time <= dt <= end_time, "Time range filter violated"
- finally:
-- for supplier_obj, dn in paused_agreements:
-- try:
-- supplier_obj.agreement.resume(dn)
-- except Exception as e:
-- log.warning(f"Failed to resume agreement {dn}: {e}")
-+ _resume_agreements(paused_agreements)
-+ _cleanup_test_data(test_users, tmp_dir)
-+
-+
-+def test_replication_log_monitoring_csn_details_edge_cases(topo_m4):
-+ """Test CSN details edge cases and structure validation
-+
-+ :id: f43dc473-4428-4971-be4c-169c4a78726e
-+ :setup: Four suppliers replication setup
-+ :steps:
-+ 1. Test CSN details structure with various replication patterns
-+ 2. Verify arrivals ordering and hop lag calculations
-+ 3. Test partial replication scenarios
-+ 4. Verify origin server detection
-+ :expectedresults:
-+ 1. CSN details should have correct structure
-+ 2. Arrivals should be ordered by timestamp
-+ 3. Partial replication should be detected
-+ 4. Origin server should be correctly identified
-+ """
-+ tmp_dir = tempfile.mkdtemp(prefix='repl_csn_edge_')
-+ test_users = []
-+ suppliers = [topo_m4.ms[f"supplier{i}"] for i in range(1, 5)]
-+
-+ try:
-+ _clear_access_logs(suppliers)
-+
-+ log.info('Creating test data for CSN details edge case testing...')
-+ test_users = _generate_test_data(suppliers[0], DEFAULT_SUFFIX, 5)
-+
-+ repl = ReplicationManager(DEFAULT_SUFFIX)
-+ repl.test_replication_topology(topo_m4)
-+
-+ _restart_suppliers(suppliers)
-+
-+ log_dirs = _get_log_dirs(suppliers)
-+ repl_monitor = ReplicationLogAnalyzer(
-+ log_dirs=log_dirs,
-+ suffixes=[DEFAULT_SUFFIX],
-+ anonymous=False,
-+ only_fully_replicated=True
-+ )
-+
-+ repl_monitor.parse_logs()
-+ generated_files = repl_monitor.generate_report(
-+ output_dir=tmp_dir,
-+ formats=['json'],
-+ report_name='csn_edge_test'
-+ )
-+
-+ assert os.path.exists(generated_files['json'])
-+
-+ json_data = _load_json(generated_files['json'])
-+
-+ assert 'csnDetails' in json_data, "csnDetails should be present"
-+ csn_details = json_data['csnDetails']
-+
-+ if csn_details:
-+ for csn, details in csn_details.items():
-+ arrivals = details.get('arrivals', [])
-+ if len(arrivals) > 1:
-+ timestamps = [a['timestamp'] for a in arrivals]
-+ assert timestamps == sorted(timestamps), \
-+ f"Arrivals for CSN {csn} should be ordered by timestamp"
-+
-+ if arrivals:
-+ origin_arrival = next((a for a in arrivals if a.get('isOrigin')), None)
-+ assert origin_arrival is not None, "Expected an arrival marked as origin"
-+ assert origin_arrival.get('server') == details.get('originServer'), \
-+ "Origin arrival server should match originServer field"
-+
-+ for i, arrival in enumerate(arrivals[1:], start=1):
-+ assert 'hopLag' in arrival, \
-+ f"Arrival {i} should have hopLag"
-+ assert arrival['hopLag'] >= 0, \
-+ "hopLag should be non-negative"
-+
-+ if len(arrivals) > 1:
-+ global_lag = details.get('globalLag', 0)
-+ assert global_lag >= 0, "globalLag should be non-negative"
-+
-+ last_delay = arrivals[-1].get('relativeDelay', 0)
-+ assert abs(last_delay - global_lag) < 0.001, \
-+ "Last arrival's relativeDelay should match globalLag"
-+
-+ server_count = details.get('serverCount', 0)
-+ assert server_count == len(arrivals), \
-+ "serverCount should match number of arrivals"
-+
-+ total_hops = details.get('totalHops', 0)
-+ expected_hops = max(0, len(arrivals) - 1)
-+ assert total_hops == expected_hops, \
-+ f"totalHops should be {expected_hops}, got {total_hops}"
-+
-+ hops = details.get('hops', [])
-+ assert len(hops) == total_hops, \
-+ "hops list length should match totalHops"
-+
-+ if 'replicationLags' in json_data and json_data['replicationLags'].get('series'):
-+ for series in json_data['replicationLags']['series']:
-+ for datapoint in series.get('datapoints', []):
-+ csn_id = datapoint.get('csnId')
-+ if csn_id:
-+ assert csn_id in csn_details, \
-+ f"csnId {csn_id} in datapoint should exist in csnDetails"
-+
-+ finally:
-+ _cleanup_test_data(test_users, tmp_dir)
-+
-+
-+def test_replication_log_monitoring_origin_out_of_scope(topo_m4):
-+ """Test origin detection when origin server record is outside time range
-+
-+ :id: d73fd9c5-f930-47d6-ade4-ada1cf0d2c21
-+ :setup: Four suppliers replication setup
-+ :steps:
-+ 1. Pause outbound agreements from the origin supplier
-+ 2. Generate changes, then resume agreements to delay consumer arrivals
-+ 3. Use time range that excludes the origin log but includes consumer logs
-+ :expectedresults:
-+ 1. Origin server should be identified from replica ID mapping
-+ 2. At least one CSN should have origin outside the selected time range
-+ 3. JSON report should include csnDetails entries for validation
-+ """
-+ tmp_dir = tempfile.mkdtemp(prefix='repl_origin_scope_')
-+ test_users = []
-+ suppliers = [topo_m4.ms[f"supplier{i}"] for i in range(1, 5)]
-+ paused_agreements = []
-+
-+ try:
-+ # Reset access logs to make time-range cuts easier to reason about
-+ _clear_access_logs(suppliers)
-+
-+ # Pause outbound agreements so consumer logs won't see the pre-resume CSNs
-+ paused_agreements = _pause_agreements(suppliers[0], DEFAULT_SUFFIX)
-+
-+ log.info('Creating pre-resume changes for origin out-of-scope test...')
-+ # Create CSNs that originate on supplier1 but won't reach consumers yet
-+ pre_start = datetime.now(timezone.utc)
-+ test_users = _generate_test_data(
-+ suppliers[0], DEFAULT_SUFFIX, 2, user_prefix="origin_scope_pre"
-+ )
-+ pre_end = datetime.now(timezone.utc)
-+ # Locate the latest origin log time for these CSNs to set a deterministic cutoff
-+ origin_log_time = _find_latest_logtime_for_prefix(
-+ suppliers[0].ds_paths.log_dir,
-+ DEFAULT_SUFFIX,
-+ pre_start,
-+ pre_end,
-+ "origin_scope_pre_"
-+ )
-+ assert origin_log_time is not None, "Expected origin server log entries for pre-resume data"
-+ # Cut the analysis window just after origin logging, excluding supplier1 entries
-+ start_time = origin_log_time + timedelta(seconds=1)
-+ time.sleep(1)
-+
-+ # Resume agreements so the pre-resume CSNs replicate to consumers after start_time
-+ _resume_agreements(paused_agreements)
-+ paused_agreements.clear()
-+
-+ log.info('Creating post-resume changes for origin mapping...')
-+ # Additional CSNs after resume ensure normal replication continues
-+ test_users += _generate_test_data(
-+ suppliers[0], DEFAULT_SUFFIX, 2, user_prefix="origin_scope_post"
-+ )
-+
-+ # Wait for replication to finish and capture the upper bound of the time window
-+ repl = ReplicationManager(DEFAULT_SUFFIX)
-+ repl.test_replication_topology(topo_m4)
-+ end_time = datetime.now(timezone.utc)
-+
-+ # Restart to flush logs before analysis
-+ _restart_suppliers(suppliers)
-+
-+ log_dirs = _get_log_dirs(suppliers)
-+ repl_monitor = ReplicationLogAnalyzer(
-+ log_dirs=log_dirs,
-+ suffixes=[DEFAULT_SUFFIX],
-+ time_range={'start': start_time, 'end': end_time}
-+ )
-+
-+ # Parse logs within the time window and produce JSON details
-+ repl_monitor.parse_logs()
-+ generated_files = repl_monitor.generate_report(
-+ output_dir=tmp_dir,
-+ formats=['json'],
-+ report_name='origin_scope_test'
-+ )
-+
-+ json_data = _load_json(generated_files['json'])
-+
-+ csn_details = json_data.get('csnDetails', {})
-+ origin_server = suppliers[0].serverid
-+ found = False
-+ if csn_details:
-+ origin_counts = {}
-+ for details in csn_details.values():
-+ origin = details.get('originServer', 'unknown')
-+ origin_counts[origin] = origin_counts.get(origin, 0) + 1
-+ if origin_server not in origin_counts and f"slapd-{origin_server}" in origin_counts:
-+ origin_server = f"slapd-{origin_server}"
-+
-+ # Focus on the pre-resume CSNs; these should have origin out of scope
-+ pre_details = [
-+ details for details in csn_details.values()
-+ if "origin_scope_pre_" in (details.get('targetDn') or '')
-+ ]
-+ assert pre_details, "Expected pre-resume CSNs in csnDetails"
-+ # Confirm at least one CSN shows origin server missing from arrivals
-+ for details in pre_details:
-+ if details.get('originServer') != origin_server:
-+ continue
-+ arrivals = details.get('arrivals', [])
-+ arrival_servers = {a.get('server') for a in arrivals}
-+ if arrival_servers and origin_server not in arrival_servers:
-+ found = True
-+ break
-+ log.info(
-+ "Origin out-of-scope candidate: csn=%s arrivals=%s",
-+ details.get('csn'),
-+ sorted(arrival_servers)
-+ )
-+
-+ assert found, (
-+ "Expected at least one CSN where the origin server is outside the time range "
-+ "but still identified via replica ID mapping"
-+ )
-+
-+ finally:
-+ _resume_agreements(paused_agreements)
-+ _cleanup_test_data(test_users, tmp_dir)
-+
-+
-+def test_replication_log_monitoring_partial_replication(topo_m4):
-+ """Test CSN details with partial replication (not all servers reached)
-+
-+ :id: d4026fd0-d83b-400e-8c2e-44fcf676368f
-+ :setup: Four suppliers replication setup
-+ :steps:
-+ 1. Pause replication agreements to create partial replication
-+ 2. Generate changes and verify partial replication detection
-+ 3. Verify replicatedToAll flag is correct
-+ :expectedresults:
-+ 1. Partial replication should be detected
-+ 2. replicatedToAll should be False for partially replicated CSNs
-+ 3. serverCount should reflect actual servers reached
-+ """
-+ tmp_dir = tempfile.mkdtemp(prefix='repl_partial_')
-+ test_users = []
-+ suppliers = [topo_m4.ms[f"supplier{i}"] for i in range(1, 5)]
-+ paused_agreements = []
-+
-+ try:
-+ _clear_access_logs(suppliers)
-+
-+ log.info('Creating fully replicated test data...')
-+ test_users = _generate_test_data(suppliers[0], DEFAULT_SUFFIX, 3)
-+
-+ repl = ReplicationManager(DEFAULT_SUFFIX)
-+ repl.test_replication_topology(topo_m4)
-+
-+ _restart_suppliers(suppliers)
-+
-+ # Pause outbound agreements from supplier1 to create partial replication
-+ paused_agreements = _pause_agreements(suppliers[0], DEFAULT_SUFFIX)
-+
-+ log.info('Creating partially replicated test data...')
-+ test_users += _generate_test_data(suppliers[0], DEFAULT_SUFFIX, 3, user_prefix="partial_user")
-+
-+ # Allow some time for local logging; do not wait for full replication
-+ time.sleep(2)
-+
-+ log_dirs = _get_log_dirs(suppliers)
-+ repl_monitor = ReplicationLogAnalyzer(
-+ log_dirs=log_dirs,
-+ suffixes=[DEFAULT_SUFFIX],
-+ anonymous=False,
-+ only_fully_replicated=False,
-+ only_not_replicated=False
-+ )
-+
-+ repl_monitor.parse_logs()
-+ generated_files = repl_monitor.generate_report(
-+ output_dir=tmp_dir,
-+ formats=['json'],
-+ report_name='partial_repl_test'
-+ )
-+
-+ json_data = _load_json(generated_files['json'])
-+
-+ assert 'csnDetails' in json_data
-+ csn_details = json_data['csnDetails']
-+
-+ if csn_details:
-+ fully_replicated_count = sum(
-+ 1 for details in csn_details.values()
-+ if details.get('replicatedToAll', False)
-+ )
-+
-+ assert fully_replicated_count > 0, \
-+ "Should have some fully replicated CSNs"
-+
-+ total_servers = len(suppliers)
-+ for csn, details in csn_details.items():
-+ server_count = details.get('serverCount', 0)
-+ assert server_count <= total_servers, \
-+ f"serverCount ({server_count}) should not exceed total servers ({total_servers})"
-+
-+ replicated_to_all = details.get('replicatedToAll', False)
-+ if replicated_to_all:
-+ assert server_count == total_servers, \
-+ "replicatedToAll=True requires serverCount == total_servers"
-+
-+ finally:
-+ _resume_agreements(paused_agreements)
- _cleanup_test_data(test_users, tmp_dir)
-
-
-diff --git a/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx b/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx
-index facbd9f5f..3c5a46a5b 100644
---- a/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx
-+++ b/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx
-@@ -3,17 +3,22 @@ import React from "react";
- import {
- Button,
- Checkbox,
-+ ClipboardCopy,
-+ ClipboardCopyVariant,
- EmptyState,
- EmptyStateIcon,
- EmptyStateBody,
- Grid,
- GridItem,
- Form,
-+ Label,
- Modal,
- ModalVariant,
- NumberInput,
- Radio,
- Spinner,
-+ Split,
-+ SplitItem,
- Tab,
- Tabs,
- TabTitleText,
-@@ -36,9 +41,12 @@ import {
- ListItem
- } from "@patternfly/react-core";
- import {
-+ ArrowRightIcon,
-+ CheckCircleIcon,
- CopyIcon,
- OutlinedQuestionCircleIcon,
- DownloadIcon,
-+ ServerIcon
- } from '@patternfly/react-icons';
- import PropTypes from "prop-types";
- import { get_date_string } from "../tools.jsx";
-@@ -71,6 +79,19 @@ const MAX_REPORT_JSON_SIZE = 64 * 1024 * 1024; // 64 MiB
- const MAX_BINARY_READ_SIZE = 64 * 1024 * 1024; // 64 MiB
- const CSV_PREVIEW_LINES = 20;
-
-+const formatLagSeconds = (seconds, precision = 3) => {
-+ if (seconds === undefined || seconds === null) {
-+ return null;
-+ }
-+ if (seconds >= 3600) {
-+ return `${(seconds / 3600).toFixed(precision)}h`;
-+ }
-+ if (seconds >= 60) {
-+ return `${(seconds / 60).toFixed(precision)}m`;
-+ }
-+ return `${seconds.toFixed(precision)}s`;
-+};
-+
- class TaskLogModal extends React.Component {
- render() {
- const {
-@@ -1168,6 +1189,13 @@ class ScatterLineChart extends React.PureComponent {
- }, 250);
- };
- this.toggleLegendItem = this.toggleLegendItem.bind(this);
-+ this.handlePointClick = this.handlePointClick.bind(this);
-+ }
-+
-+ handlePointClick(datum, seriesIndex) {
-+ if (this.props.onPointClick && datum.csnId) {
-+ this.props.onPointClick(datum);
-+ }
- }
-
- componentDidMount() {
-@@ -1267,15 +1295,7 @@ class ScatterLineChart extends React.PureComponent {
- const { series, yDomain } = this._getSeriesSnapshot();
-
- // Helper function to format time values
-- const formatTimeValue = (seconds) => {
-- if (seconds >= 3600) {
-- return `${(seconds / 3600).toFixed(3)}h`;
-- } else if (seconds >= 60) {
-- return `${(seconds / 60).toFixed(3)}m`;
-- } else {
-- return `${seconds.toFixed(3)}s`;
-- }
-- };
-+ const formatTimeValue = (seconds) => formatLagSeconds(seconds, 3);
-
- // Process tooltip HTML tags
- const formatTooltip = (datum) => {
-@@ -1308,13 +1328,35 @@ class ScatterLineChart extends React.PureComponent {
- labels={({ datum }) => formatTooltip(datum)}
- constrainToVisibleArea
- labelComponent={
-- <ChartTooltip
-- style={{
-- fontSize: "12px",
-- padding: 10,
-- whiteSpace: "pre-line" // Important for newlines
-- }}
-- />
-+ <ChartTooltip
-+ orientation={({ datum }) => {
-+ // Position tooltip below for high points, above for low points
-+ // This prevents the tooltip from blocking clicks on points near the top
-+ const yMax = yDomain.max;
-+ const yMin = yDomain.min;
-+ const yRange = yMax - yMin;
-+ const threshold = yMin + (yRange * 0.6);
-+ return datum.y > threshold ? "bottom" : "top";
-+ }}
-+ style={{
-+ fontSize: "12px",
-+ padding: 10,
-+ whiteSpace: "pre-line", // Important for newlines
-+ pointerEvents: "none"
-+ }}
-+ flyoutStyle={{
-+ pointerEvents: "none"
-+ }}
-+ dx={0}
-+ dy={({ datum }) => {
-+ // Add extra offset to keep tooltip away from the point
-+ const yMax = yDomain.max;
-+ const yMin = yDomain.min;
-+ const yRange = yMax - yMin;
-+ const threshold = yMin + (yRange * 0.6);
-+ return datum.y > threshold ? 10 : -10;
-+ }}
-+ />
- }
- />
- }
-@@ -1410,6 +1452,7 @@ class ScatterLineChart extends React.PureComponent {
- if (this.state.hiddenSeries[idx]) {
- return null;
- }
-+ const hasClickHandler = !!this.props.onPointClick;
- return (
- <ChartScatter
- key={`scatter-${idx}`}
-@@ -1417,9 +1460,39 @@ class ScatterLineChart extends React.PureComponent {
- data={s.datapoints}
- style={{
- data: {
-- fill: s.color
-+ fill: s.color,
-+ cursor: hasClickHandler ? 'pointer' : 'default'
- }
- }}
-+ events={hasClickHandler ? [{
-+ target: "data",
-+ eventHandlers: {
-+ onClick: () => [{
-+ target: "data",
-+ mutation: (props) => {
-+ this.handlePointClick(props.datum, idx);
-+ return null;
-+ }
-+ }],
-+ onMouseOver: () => [{
-+ target: "data",
-+ mutation: (props) => ({
-+ style: {
-+ ...props.style,
-+ fill: s.color,
-+ cursor: 'pointer',
-+ strokeWidth: 2,
-+ stroke: 'var(--pf-v5-global--active-color--100, #0066cc)',
-+ r: 6
-+ }
-+ })
-+ }],
-+ onMouseOut: () => [{
-+ target: "data",
-+ mutation: () => null
-+ }]
-+ }
-+ }] : undefined}
- />
- );
- })}
-@@ -1501,17 +1574,320 @@ class ScatterLineChart extends React.PureComponent {
- }
- }
-
-+/**
-+ * CSNDetailModal - Displays detailed CSN propagation path information
-+ * Shows the hop-by-hop timing of how a change propagated through the replication topology
-+ */
-+class CSNDetailModal extends React.Component {
-+ constructor(props) {
-+ super(props);
-+ this.formatTimestamp = this.formatTimestamp.bind(this);
-+ this.formatLag = this.formatLag.bind(this);
-+ }
-+
-+ formatTimestamp(isoString) {
-+ if (!isoString) return _("Unknown");
-+ try {
-+ const date = new Date(isoString);
-+ if (isNaN(date.getTime())) {
-+ console.warn("Invalid timestamp format:", isoString);
-+ return cockpit.format(_("Invalid: $0"), isoString);
-+ }
-+ return date.toLocaleString(undefined, {
-+ year: 'numeric',
-+ month: '2-digit',
-+ day: '2-digit',
-+ hour: '2-digit',
-+ minute: '2-digit',
-+ second: '2-digit',
-+ fractionalSecondDigits: 3,
-+ hour12: false
-+ });
-+ } catch (e) {
-+ console.warn("Error formatting timestamp:", isoString, e);
-+ return cockpit.format(_("Invalid: $0"), isoString);
-+ }
-+ }
-+
-+ formatLag(seconds) {
-+ return formatLagSeconds(seconds, 3) || _("N/A");
-+ }
-+
-+ render() {
-+ const { csnData, onClose } = this.props;
-+
-+ if (!csnData) {
-+ return null;
-+ }
-+
-+ const arrivals = csnData.arrivals || [];
-+ const pathJson = JSON.stringify(csnData, null, 2);
-+
-+ return (
-+ <Modal
-+ variant={ModalVariant.large}
-+ title={_("CSN Propagation Details")}
-+ isOpen={!!csnData}
-+ onClose={onClose}
-+ aria-label={_("CSN propagation details")}
-+ actions={[
-+ <Button key="close" variant="primary" onClick={onClose}>
-+ {_("Close")}
-+ </Button>
-+ ]}
-+ >
-+ {/* CSN Summary Information */}
-+ <Card isFlat className="ds-margin-bottom-md">
-+ <CardBody>
-+ <Grid hasGutter>
-+ <GridItem span={6}>
-+ <DescriptionList isHorizontal isCompact>
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("CSN")}</DescriptionListTerm>
-+ <DescriptionListDescription>
-+ <ClipboardCopy
-+ variant={ClipboardCopyVariant.inline}
-+ >
-+ {csnData.csn}
-+ </ClipboardCopy>
-+ </DescriptionListDescription>
-+ </DescriptionListGroup>
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("Entry DN")}</DescriptionListTerm>
-+ <DescriptionListDescription>
-+ <Tooltip content={csnData.targetDn}>
-+ <span className="pf-v5-u-text-truncate" style={{ maxWidth: '300px', display: 'inline-block' }}>
-+ {csnData.targetDn}
-+ </span>
-+ </Tooltip>
-+ </DescriptionListDescription>
-+ </DescriptionListGroup>
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("Suffix")}</DescriptionListTerm>
-+ <DescriptionListDescription>{csnData.suffix}</DescriptionListDescription>
-+ </DescriptionListGroup>
-+ </DescriptionList>
-+ </GridItem>
-+ <GridItem span={6}>
-+ <DescriptionList isHorizontal isCompact>
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("Origin Server")}</DescriptionListTerm>
-+ <DescriptionListDescription>
-+ <Label color="blue" icon={<ServerIcon />}>
-+ {csnData.originServer}
-+ </Label>
-+ </DescriptionListDescription>
-+ </DescriptionListGroup>
-+ {csnData.originIncludedInArrivals === false && (
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("Origin Note")}</DescriptionListTerm>
-+ <DescriptionListDescription>
-+ <Text component={TextVariants.small} style={{ color: 'var(--pf-v5-global--Color--200)' }}>
-+ {_("Origin server record is outside the selected time range; entry details reflect the earliest arrival.")}
-+ </Text>
-+ </DescriptionListDescription>
-+ </DescriptionListGroup>
-+ )}
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("Total Lag")}</DescriptionListTerm>
-+ <DescriptionListDescription>
-+ <strong>{this.formatLag(csnData.globalLag)}</strong>
-+ </DescriptionListDescription>
-+ </DescriptionListGroup>
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("Servers Reached")}</DescriptionListTerm>
-+ <DescriptionListDescription>
-+ {csnData.serverCount}
-+ {csnData.replicatedToAll && (
-+ <Label color="green" icon={<CheckCircleIcon />} className="ds-left-margin">
-+ {_("All")}
-+ </Label>
-+ )}
-+ </DescriptionListDescription>
-+ </DescriptionListGroup>
-+ </DescriptionList>
-+ </GridItem>
-+ </Grid>
-+ </CardBody>
-+ </Card>
-+
-+ {/* Arrival Timeline Visualization */}
-+ <Card isFlat className="ds-margin-bottom-md">
-+ <CardTitle>{_("Arrival Timeline")}</CardTitle>
-+ <CardBody>
-+ <Text
-+ component={TextVariants.small}
-+ className="ds-margin-bottom"
-+ style={{ color: 'var(--pf-v5-global--Color--200)', fontStyle: 'italic' }}
-+ >
-+ {_("Note: Shows arrival order by time. Actual replication topology may differ in fan-out configurations.")}
-+ </Text>
-+ <div
-+ role="list"
-+ aria-label={_("CSN propagation timeline")}
-+ style={{
-+ display: 'flex',
-+ flexWrap: 'wrap',
-+ alignItems: 'center',
-+ gap: 'var(--pf-v5-global--spacer--sm)',
-+ padding: 'var(--pf-v5-global--spacer--sm)'
-+ }}
-+ >
-+ {arrivals.map((arrival, idx) => (
-+ <React.Fragment key={idx}>
-+ {/* Server Node */}
-+ <div
-+ role="listitem"
-+ aria-label={cockpit.format(
-+ arrival.isOrigin
-+ ? _("Origin server: $0")
-+ : _("Server $0, delay: $1"),
-+ arrival.server,
-+ arrival.isOrigin ? "" : this.formatLag(arrival.relativeDelay)
-+ )}
-+ style={{
-+ display: 'flex',
-+ flexDirection: 'column',
-+ alignItems: 'center',
-+ padding: 'var(--pf-v5-global--spacer--sm)',
-+ backgroundColor: arrival.isOrigin
-+ ? 'var(--pf-v5-global--palette--blue-50, #e7f1fa)'
-+ : 'var(--pf-v5-global--BackgroundColor--200, #f0f0f0)',
-+ borderRadius: 'var(--pf-v5-global--BorderRadius--sm)',
-+ border: arrival.isOrigin
-+ ? '2px solid var(--pf-v5-global--primary-color--100, #0066cc)'
-+ : '1px solid var(--pf-v5-global--BorderColor--100, #d2d2d2)',
-+ minWidth: '120px'
-+ }}>
-+ <Text component={TextVariants.small} style={{ fontWeight: 'bold' }}>
-+ {arrival.server}
-+ </Text>
-+ <Text component={TextVariants.small} style={{ fontSize: '0.75rem', color: 'var(--pf-v5-global--Color--200)' }}>
-+ {this.formatTimestamp(arrival.timestamp)}
-+ </Text>
-+ {arrival.isOrigin && (
-+ <Label color="blue" isCompact style={{ marginTop: '4px' }}>
-+ {_("Origin")}
-+ </Label>
-+ )}
-+ {!arrival.isOrigin && (
-+ <Text component={TextVariants.small} style={{ marginTop: '4px', color: 'var(--pf-v5-global--success-color--100)' }}>
-+ +{this.formatLag(arrival.relativeDelay)}
-+ </Text>
-+ )}
-+ </div>
-+
-+ {/* Arrow between nodes */}
-+ {idx < arrivals.length - 1 && (
-+ <div
-+ role="presentation"
-+ aria-hidden="true"
-+ style={{
-+ display: 'flex',
-+ flexDirection: 'column',
-+ alignItems: 'center',
-+ padding: '0 var(--pf-v5-global--spacer--xs)'
-+ }}
-+ >
-+ <ArrowRightIcon style={{ color: 'var(--pf-v5-global--Color--200)' }} />
-+ <Text component={TextVariants.small} style={{
-+ fontSize: '0.7rem',
-+ color: 'var(--pf-v5-global--Color--200)',
-+ whiteSpace: 'nowrap'
-+ }}>
-+ {arrivals[idx + 1].hopLag !== undefined
-+ ? this.formatLag(arrivals[idx + 1].hopLag)
-+ : ''}
-+ </Text>
-+ </div>
-+ )}
-+ </React.Fragment>
-+ ))}
-+ </div>
-+ </CardBody>
-+ </Card>
-+
-+ {/* Detailed Arrivals Table */}
-+ <Card isFlat className="ds-margin-bottom-md">
-+ <CardTitle>{_("Arrival Details")}</CardTitle>
-+ <CardBody>
-+ <table className="pf-v5-c-table pf-m-compact" role="grid">
-+ <thead>
-+ <tr>
-+ <th>{_("Server")}</th>
-+ <th>{_("Arrival Time")}</th>
-+ <th>{_("Hop Lag")}</th>
-+ <th>{_("Cumulative Delay")}</th>
-+ <th>{_("Duration")}</th>
-+ </tr>
-+ </thead>
-+ <tbody>
-+ {arrivals.map((arrival, idx) => (
-+ <tr key={idx}>
-+ <td>
-+ {arrival.server}
-+ {arrival.isOrigin && (
-+ <span style={{ color: 'var(--pf-v5-global--primary-color--100)', marginLeft: 'var(--pf-v5-global--spacer--xs)' }}>
-+ ({_("Origin")})
-+ </span>
-+ )}
-+ </td>
-+ <td>{this.formatTimestamp(arrival.timestamp)}</td>
-+ <td>
-+ {arrival.isOrigin
-+ ? <em>{_("Origin")}</em>
-+ : this.formatLag(arrival.hopLag)}
-+ </td>
-+ <td>{this.formatLag(arrival.relativeDelay)}</td>
-+ <td>{arrival.duration ? this.formatLag(arrival.duration) : _("N/A")}</td>
-+ </tr>
-+ ))}
-+ </tbody>
-+ </table>
-+ </CardBody>
-+ </Card>
-+
-+ {/* Copy Actions */}
-+ <Split hasGutter>
-+ <SplitItem>
-+ <ClipboardCopy
-+ variant={ClipboardCopyVariant.expansion}
-+ isExpanded={false}
-+ isCode
-+ isReadOnly
-+ hoverTip={_("Copy full path JSON")}
-+ clickTip={_("Copied!")}
-+ >
-+ {pathJson}
-+ </ClipboardCopy>
-+ </SplitItem>
-+ </Split>
-+ </Modal>
-+ );
-+ }
-+}
-+
-+CSNDetailModal.propTypes = {
-+ csnData: PropTypes.object,
-+ onClose: PropTypes.func.isRequired
-+};
-+
-+CSNDetailModal.defaultProps = {
-+ csnData: null
-+};
-+
- class LagReportModal extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
-- activeTabKey: 0, // 0 = Summary, 1 = Charts, 2 = PNG Report, 3 = CSV Report, 4 = Report Files
-+ activeTabKey: 0,
- ...this._freshReportState(),
- loadingSummary: false,
- loadingJson: false,
- loadingCsv: false,
-- loadingPng: false
-+ loadingPng: false,
-+ selectedCsnId: null
- };
-
- this.handleTabClick = this.handleTabClick.bind(this);
-@@ -1524,6 +1900,8 @@ class LagReportModal extends React.Component {
- this.renderPngTab = this.renderPngTab.bind(this);
- this.renderCsvTab = this.renderCsvTab.bind(this);
- this.renderReportFilesTab = this.renderReportFilesTab.bind(this);
-+ this.handleCsnPointClick = this.handleCsnPointClick.bind(this);
-+ this.handleCloseCsnDetails = this.handleCloseCsnDetails.bind(this);
-
- this._activeLoadToken = 0;
- this._isMounted = false;
-@@ -1543,10 +1921,26 @@ class LagReportModal extends React.Component {
- summary: null,
- suffixStats: {},
- clientSamplingNotice: null,
-+ selectedCsnId: null,
- ...overrides
- };
- }
-
-+ handleCsnPointClick(datum) {
-+ if (datum && datum.csnId) {
-+ const { jsonData } = this.state;
-+ if (jsonData && jsonData.csnDetails && jsonData.csnDetails[datum.csnId]) {
-+ this.setState({ selectedCsnId: datum.csnId });
-+ } else {
-+ console.warn("CSN details not available for:", datum.csnId);
-+ }
-+ }
-+ }
-+
-+ handleCloseCsnDetails() {
-+ this.setState({ selectedCsnId: null });
-+ }
-+
- componentDidMount() {
- this._isMounted = true;
- this.loadData();
-@@ -2117,7 +2511,7 @@ class LagReportModal extends React.Component {
-
- renderChartsTab() {
- const { reportUrls } = this.props;
-- const { loadingJson, jsonData, error, clientSamplingNotice } = this.state;
-+ const { loadingJson, jsonData, error, clientSamplingNotice, selectedCsnId } = this.state;
-
- if (loadingJson) {
- return (
-@@ -2170,6 +2564,13 @@ class LagReportModal extends React.Component {
- jsonData.hopLags.series &&
- jsonData.hopLags.series.length > 0;
-
-+ const hasCsnDetails = jsonData && jsonData.csnDetails &&
-+ Object.keys(jsonData.csnDetails).length > 0;
-+
-+ const selectedCsnData = selectedCsnId && hasCsnDetails
-+ ? jsonData.csnDetails[selectedCsnId]
-+ : null;
-+
- if (!jsonData || (!hasReplicationLags && !hasHopLags)) {
- return (
- <EmptyState>
-@@ -2201,6 +2602,15 @@ class LagReportModal extends React.Component {
- {clientSamplingNotice}
- </Alert>
- )}
-+ {hasCsnDetails && (
-+ <Text
-+ component={TextVariants.small}
-+ className="ds-margin-bottom"
-+ style={{ color: 'var(--pf-v5-global--Color--200)', fontStyle: 'italic' }}
-+ >
-+ {_("Tip: Click on any chart point to view detailed CSN propagation path.")}
-+ </Text>
-+ )}
- {hasReplicationLags && (
- <div className="ds-margin-bottom">
- <Title headingLevel="h3">
-@@ -2231,6 +2641,7 @@ class LagReportModal extends React.Component {
- xAxisLabel={(jsonData.replicationLags.xAxisLabel || "").replace(/\s*Time\s*/g, "")}
- yAxisLabel={jsonData.replicationLags.yAxisLabel || _("Lag Time (seconds)")}
- defaultShowLegend={true}
-+ onPointClick={hasCsnDetails ? this.handleCsnPointClick : undefined}
- />
- </div>
- )}
-@@ -2250,9 +2661,17 @@ class LagReportModal extends React.Component {
- xAxisLabel={(jsonData.hopLags.xAxisLabel || "").replace(/\s*Time\s*/g, "")}
- yAxisLabel={jsonData.hopLags.yAxisLabel || _("Hop Lag Time (seconds)")}
- defaultShowLegend={false}
-+ onPointClick={hasCsnDetails ? this.handleCsnPointClick : undefined}
- />
- </div>
- )}
-+
-+ {/* CSN Detail Panel - shown when a point is clicked */}
-+ <CSNDetailModal
-+ csnData={selectedCsnData}
-+ onClose={this.handleCloseCsnDetails}
-+ />
-+
- <div className="ds-margin-top">
- <Button
- variant="secondary"
-@@ -2876,6 +3295,28 @@ class ChooseLagReportModal extends React.Component {
- }
-
- // Prototypes and defaultProps
-+ScatterLineChart.propTypes = {
-+ chartData: PropTypes.object,
-+ title: PropTypes.string,
-+ yAxisLabel: PropTypes.string,
-+ xAxisLabel: PropTypes.string,
-+ minY: PropTypes.number,
-+ maxY: PropTypes.number,
-+ defaultShowLegend: PropTypes.bool,
-+ onPointClick: PropTypes.func
-+};
-+
-+ScatterLineChart.defaultProps = {
-+ chartData: null,
-+ title: "",
-+ yAxisLabel: "Value",
-+ xAxisLabel: "",
-+ minY: null,
-+ maxY: null,
-+ defaultShowLegend: true,
-+ onPointClick: null
-+};
-+
- AgmtDetailsModal.propTypes = {
- showModal: PropTypes.bool,
- closeHandler: PropTypes.func,
-@@ -3044,6 +3485,7 @@ export {
- ReportLoginModal,
- FullReportContent,
- LagReportModal,
-- ChooseLagReportModal
-+ ChooseLagReportModal,
-+ CSNDetailModal
- };
-
-diff --git a/src/lib389/lib389/repltools.py b/src/lib389/lib389/repltools.py
-index 9d1aa4058..5ab8b3187 100644
---- a/src/lib389/lib389/repltools.py
-+++ b/src/lib389/lib389/repltools.py
-@@ -683,6 +683,7 @@ class ChartData(NamedTuple):
- lags: List[float]
- durations: List[float]
- hover: List[str]
-+ csn_ids: List[str]
-
- class VisualizationHelper:
- """Helper class for visualization-related functionality."""
-@@ -730,7 +731,7 @@ class VisualizationHelper:
- tz: tzinfo = timezone.utc) -> Dict[Tuple[str, str], ChartData]:
- """Prepare data for visualization with timezone-aware timestamps."""
- chart_data = defaultdict(lambda: {
-- 'times': [], 'lags': [], 'durations': [], 'hover': []
-+ 'times': [], 'lags': [], 'durations': [], 'hover': [], 'csn_ids': []
- })
-
- for csn, server_map in csns.items():
-@@ -766,6 +767,7 @@ class VisualizationHelper:
- data_slot['times'].append(ts_dt)
- data_slot['lags'].append(lag_val) # The same global-lag for all servers
- data_slot['durations'].append(duration_val)
-+ data_slot['csn_ids'].append(csn)
- # Format timestamp for hover display in the specified timezone
- timestamp_str = ts_dt.strftime('%Y-%m-%d %H:%M:%S')
- data_slot['hover'].append(
-@@ -784,7 +786,8 @@ class VisualizationHelper:
- times=value['times'],
- lags=value['lags'],
- durations=value['durations'],
-- hover=value['hover']
-+ hover=value['hover'],
-+ csn_ids=value['csn_ids']
- )
- for key, value in chart_data.items()
- }
-@@ -803,6 +806,14 @@ class ReplicationLogAnalyzer:
- AUTO_SAMPLING_THRESHOLD = 4000 # Trigger auto sampling above this many CSN points
- HOP_SERIES_BUDGET_RATIO = 0.25 # Allocate max 25% of chart points to hop lag series
- MIN_POINTS_PER_SERIES = 2 # Minimum points to preserve series shape after sampling
-+ MAX_CSN_DETAILS = 10000 # Maximum CSN details to include (prevents memory issues)
-+
-+ # CSN format: TTTTTTTTSSSSRRRRNNNN (20 hex chars)
-+ # T=timestamp(8), S=sequence(4), R=replicaID(4), N=subseq(4)
-+ CSN_TIMESTAMP_START = 0
-+ CSN_TIMESTAMP_END = 8
-+ CSN_REPLICA_ID_START = 12
-+ CSN_REPLICA_ID_END = 16
-
- # Precision preset configurations
- PRECISION_PRESETS = {
-@@ -867,6 +878,54 @@ class ReplicationLogAnalyzer:
- # Threshold to trigger auto sampling if lots of CSNs
- self._auto_sampling_csn_threshold = self.AUTO_SAMPLING_THRESHOLD
-
-+ # Mapping of (replica ID, suffix) to server name
-+ # Built during log parsing to correctly identify origin servers
-+ # Keyed by (replica_id, suffix) because replica IDs are per-suffix in multi-suffix deployments
-+ self._replica_id_to_server: Dict[Tuple[str, str], str] = {}
-+
-+ @staticmethod
-+ def _extract_replica_id_from_csn(csn: str) -> Optional[str]:
-+ """Extract the replica ID from a CSN string.
-+
-+ CSN format: TTTTTTTTSSSSRRRRNNNN (20 hex characters)
-+ - T = timestamp (8 chars)
-+ - S = sequence (4 chars)
-+ - R = replica ID (4 chars)
-+ - N = subseq (4 chars)
-+
-+ :param csn: The CSN string
-+ :returns: The replica ID as a string (4 hex chars), or None if invalid
-+ """
-+ if not csn or not isinstance(csn, str) or len(csn) < 16:
-+ return None
-+ try:
-+ # Extract and validate replica ID is valid hex
-+ replica_id = csn[ReplicationLogAnalyzer.CSN_REPLICA_ID_START:
-+ ReplicationLogAnalyzer.CSN_REPLICA_ID_END]
-+ int(replica_id, 16) # Validate it's valid hex
-+ return replica_id
-+ except (ValueError, IndexError):
-+ return None
-+
-+ @staticmethod
-+ def _extract_timestamp_from_csn(csn: str) -> Optional[float]:
-+ """Extract the timestamp (epoch seconds) from a CSN string.
-+
-+ CSN format: TTTTTTTTSSSSRRRRNNNN (20 hex characters)
-+ - T = timestamp (8 chars, hex, seconds since epoch)
-+
-+ :param csn: The CSN string
-+ :returns: Timestamp as float (epoch seconds), or None if invalid
-+ """
-+ if not csn or not isinstance(csn, str) or len(csn) < 8:
-+ return None
-+ try:
-+ ts_hex = csn[ReplicationLogAnalyzer.CSN_TIMESTAMP_START:
-+ ReplicationLogAnalyzer.CSN_TIMESTAMP_END]
-+ return float(int(ts_hex, 16))
-+ except (ValueError, IndexError):
-+ return None
-+
- def _should_include_record(self, csn: str, server_map: Dict[Union[int, str], Dict[str, Any]]) -> bool:
- """Determine if a record should be included based on filtering criteria."""
- total_servers = self._active_server_count or len(self.log_dirs)
-@@ -990,6 +1049,148 @@ class ReplicationLogAnalyzer:
-
- return hops
-
-+ def _build_csn_details(self, csn_whitelist: Optional[set] = None) -> Dict[str, Dict[str, Any]]:
-+ """Build detailed CSN propagation information for drill-down functionality.
-+
-+ Returns a dictionary keyed by CSN containing:
-+ - csn: The CSN string
-+ - targetDn: The target entry DN
-+ - suffix: The replication suffix
-+ - globalLag: Total propagation time (earliest to latest arrival)
-+ - originServer: The server where the change originated (determined by CSN replica ID)
-+ - originTime: ISO timestamp of origin
-+ - arrivals: Ordered list of server arrivals with timing details
-+ - hops: List of server-to-server hops with lag times
-+ - totalHops: Number of hops in the propagation path
-+ - replicatedToAll: Whether the change reached all servers
-+
-+ Note: Origin server is determined by the replica ID embedded in the CSN,
-+ not by earliest log timestamp (which can be incorrect under clock skew).
-+
-+ :param csn_whitelist: Optional set of CSN IDs to include. If provided,
-+ only these CSNs will have details generated.
-+ This prevents memory bloat when chart data is sampled.
-+ """
-+ csn_details = {}
-+ total_servers = self._active_server_count or len(self.log_dirs)
-+
-+ if csn_whitelist is not None:
-+ csn_items = [(csn, sm) for csn, sm in self.csns.items() if csn in csn_whitelist]
-+ else:
-+ csn_items = list(self.csns.items())
-+
-+ if len(csn_items) > self.MAX_CSN_DETAILS:
-+ self._logger.info(
-+ f"CSN details limited to {self.MAX_CSN_DETAILS} entries "
-+ f"(dataset has {len(csn_items)} CSNs). "
-+ "Selecting CSNs with highest global lag for drill-down."
-+ )
-+ csn_lags = []
-+ for csn, server_map in csn_items:
-+ t_list = [
-+ rec.get('logtime', 0)
-+ for key, rec in server_map.items()
-+ if isinstance(rec, dict) and key != '__hop_lags__' and 'logtime' in rec
-+ ]
-+ if t_list:
-+ lag = max(t_list) - min(t_list)
-+ csn_lags.append((csn, server_map, lag))
-+ csn_lags.sort(key=lambda x: x[2], reverse=True)
-+ csn_items = [(csn, sm) for csn, sm, _ in csn_lags[:self.MAX_CSN_DETAILS]]
-+
-+ for csn, server_map in csn_items:
-+ valid_records = []
-+ for key, data in server_map.items():
-+ if isinstance(data, dict) and key != '__hop_lags__' and 'logtime' in data:
-+ valid_records.append(data)
-+
-+ if not valid_records:
-+ continue
-+
-+ valid_records.sort(key=lambda x: x['logtime'])
-+
-+ suffix = None
-+ for rec in valid_records:
-+ if rec.get('suffix'):
-+ suffix = rec['suffix']
-+ break
-+
-+ replica_id = self._extract_replica_id_from_csn(csn)
-+ origin_server_name = None
-+ origin_record = None
-+
-+ origin_in_arrivals = False
-+ if replica_id and suffix:
-+ map_key = (replica_id, suffix)
-+ if map_key in self._replica_id_to_server:
-+ origin_server_name = self._replica_id_to_server[map_key]
-+ for rec in valid_records:
-+ if rec.get('server_name') == origin_server_name:
-+ origin_record = rec
-+ origin_in_arrivals = True
-+ break
-+
-+ if origin_record is None:
-+ origin_record = valid_records[0]
-+ if not origin_server_name:
-+ origin_server_name = origin_record.get('server_name', 'unknown')
-+
-+ csn_ts = self._extract_timestamp_from_csn(csn)
-+ origin_time = csn_ts if csn_ts is not None else origin_record['logtime']
-+ earliest_time = valid_records[0]['logtime']
-+ latest_time = valid_records[-1]['logtime']
-+ global_lag = latest_time - earliest_time
-+
-+ arrivals = []
-+ for idx, rec in enumerate(valid_records):
-+ server_name = rec.get('server_name', 'unknown')
-+ is_origin = (server_name == origin_server_name)
-+
-+ arrival_entry = {
-+ 'server': server_name,
-+ 'timestamp': datetime.fromtimestamp(rec['logtime'], tz=self.tz).isoformat(),
-+ 'relativeDelay': rec['logtime'] - earliest_time,
-+ 'duration': float(rec.get('duration', 0.0)),
-+ 'etime': rec.get('etime')
-+ }
-+
-+ if is_origin:
-+ arrival_entry['isOrigin'] = True
-+
-+ if idx > 0:
-+ prev_rec = valid_records[idx - 1]
-+ arrival_entry['hopFrom'] = prev_rec.get('server_name', 'unknown')
-+ arrival_entry['hopLag'] = rec['logtime'] - prev_rec['logtime']
-+
-+ arrivals.append(arrival_entry)
-+
-+ hops = []
-+ for i in range(1, len(valid_records)):
-+ prev_rec = valid_records[i - 1]
-+ curr_rec = valid_records[i]
-+ hops.append({
-+ 'from': prev_rec.get('server_name', 'unknown'),
-+ 'to': curr_rec.get('server_name', 'unknown'),
-+ 'lag': curr_rec['logtime'] - prev_rec['logtime']
-+ })
-+
-+ csn_details[csn] = {
-+ 'csn': csn,
-+ 'targetDn': origin_record.get('target_dn', 'unknown') or 'unknown',
-+ 'suffix': origin_record.get('suffix', 'unknown') or 'unknown',
-+ 'globalLag': global_lag,
-+ 'originServer': origin_server_name,
-+ 'originIncludedInArrivals': origin_in_arrivals,
-+ 'originTime': datetime.fromtimestamp(origin_time, tz=self.tz).isoformat(),
-+ 'arrivals': arrivals,
-+ 'hops': hops,
-+ 'totalHops': len(hops),
-+ 'serverCount': len(valid_records),
-+ 'replicatedToAll': len(valid_records) == total_servers
-+ }
-+
-+ return csn_details
-+
- def parse_logs(self) -> None:
- """Parse logs from all directories. Each directory is treated as one server
- unless anonymized, in which case we use 'server_{index}'.
-@@ -1044,6 +1245,41 @@ class ReplicationLogAnalyzer:
- 'duration': record.get('duration', 0.0),
- }
-
-+ # Build (replica ID, suffix) to server mapping based on closest CSN timestamp
-+ # Keyed by (replica_id, suffix) because replica IDs are per-suffix in multi-suffix deployments
-+ # For each (replica ID, suffix) pair, the server whose logtime is closest to the CSN
-+ # timestamp is the best origin candidate under clock skew.
-+ replica_id_best: Dict[Tuple[str, str], Tuple[bool, float, float, str]] = {}
-+ for csn, server_map in self.csns.items():
-+ replica_id = self._extract_replica_id_from_csn(csn)
-+ if not replica_id:
-+ continue
-+ csn_ts = self._extract_timestamp_from_csn(csn)
-+ for key, record in server_map.items():
-+ if not isinstance(record, dict) or key == '__hop_lags__':
-+ continue
-+ logtime = record.get('logtime')
-+ server_name = record.get('server_name')
-+ suffix = record.get('suffix')
-+ if logtime is None or not server_name or not suffix:
-+ continue
-+ # Prefer candidates where we can compare against CSN timestamp
-+ has_csn_ts = csn_ts is not None
-+ score = abs(logtime - csn_ts) if has_csn_ts else logtime
-+ map_key = (replica_id, suffix)
-+ if map_key not in replica_id_best:
-+ replica_id_best[map_key] = (has_csn_ts, score, logtime, server_name)
-+ continue
-+ prev_has_ts, prev_score, prev_logtime, _ = replica_id_best[map_key]
-+ if has_csn_ts and not prev_has_ts:
-+ replica_id_best[map_key] = (has_csn_ts, score, logtime, server_name)
-+ elif has_csn_ts == prev_has_ts:
-+ if score < prev_score or (score == prev_score and logtime < prev_logtime):
-+ replica_id_best[map_key] = (has_csn_ts, score, logtime, server_name)
-+
-+ # Store the mapping ((replica ID, suffix) -> server name)
-+ self._replica_id_to_server = {k: srv for k, (_, _, _, srv) in replica_id_best.items()}
-+
- # Apply filters after collecting all data
- filtered_csns = {}
- earliest_udt: Optional[float] = None
-@@ -1577,6 +1813,17 @@ class ReplicationLogAnalyzer:
- except Exception as e:
- raise IOError(f"Failed to write JSON summary to {outfile}: {e}")
-
-+ @staticmethod
-+ def _collect_csn_ids(series_list: List[Dict[str, Any]]) -> set:
-+ """Collect CSN IDs from chart series datapoints."""
-+ csn_ids = set()
-+ for series in series_list:
-+ for dp in series.get("datapoints", []):
-+ csn_id = dp.get("csnId")
-+ if csn_id:
-+ csn_ids.add(csn_id)
-+ return csn_ids
-+
- def _generate_patternfly_json(self, results: Dict[str, Any], outfile: str) -> None:
- """Generate JSON specifically formatted for PatternFly 5 charts."""
- chart_data = VisualizationHelper.prepare_chart_data(self.csns, self.tz)
-@@ -1637,7 +1884,8 @@ class ReplicationLogAnalyzer:
- "x": data.times[i].isoformat(),
- "y": data.lags[i],
- "duration": data.durations[i],
-- "hoverInfo": data.hover[i]
-+ "hoverInfo": data.hover[i],
-+ "csnId": data.csn_ids[i]
- } for i in indices]
- series_data.append({
- "datapoints": datapoints,
-@@ -1645,7 +1893,7 @@ class ReplicationLogAnalyzer:
- "color": color_palette[idx % len(color_palette)]
- })
-
-- hop_data: Dict[str, Dict[str, List[Any]]] = defaultdict(lambda: {"times": [], "lags": [], "hover": []})
-+ hop_data: Dict[str, Dict[str, List[Any]]] = defaultdict(lambda: {"times": [], "lags": [], "hover": [], "csn_ids": []})
- for csn, server_map in self.csns.items():
- for hop in server_map.get('__hop_lags__', []):
- source = hop.get('supplier', 'unknown')
-@@ -1655,6 +1903,7 @@ class ReplicationLogAnalyzer:
- ts = datetime.fromtimestamp(hop.get('arrival_consumer', 0.0), tz=self.tz)
- entry["times"].append(ts)
- entry["lags"].append(hop.get('hop_lag', 0.0))
-+ entry["csn_ids"].append(csn)
- ts_str = ts.strftime('%Y-%m-%d %H:%M:%S')
- entry["hover"].append(
- f"Timestamp: {ts_str}<br>"
-@@ -1689,7 +1938,10 @@ class ReplicationLogAnalyzer:
- "name": key,
- "x": entry["times"][i].isoformat(),
- "y": entry["lags"][i],
-- "hoverInfo": entry["hover"][i].replace("Suffix: None", "Suffix: unknown").replace("Entry: None", "Entry: unknown")
-+ "hoverInfo": (entry["hover"][i]
-+ .replace("Suffix: None", "Suffix: unknown")
-+ .replace("Entry: None", "Entry: unknown")),
-+ "csnId": entry["csn_ids"][i]
- } for i in indices]
- hop_series.append({
- "datapoints": datapoints,
-@@ -1702,6 +1954,13 @@ class ReplicationLogAnalyzer:
- reduced += sum(len(item["datapoints"]) for item in hop_series)
- sampling_meta["reducedTotalPoints"] = reduced
-
-+ sampled_csn_ids = None
-+ if sampling_meta["applied"]:
-+ sampled_csn_ids = self._collect_csn_ids(series_data)
-+ sampled_csn_ids.update(self._collect_csn_ids(hop_series))
-+
-+ csn_details = self._build_csn_details(csn_whitelist=sampled_csn_ids)
-+
- pf_data = {
- "replicationLags": {
- "title": "Global Replication Lag Over Time",
-@@ -1715,6 +1974,7 @@ class ReplicationLogAnalyzer:
- "xAxisLabel": "Time",
- "series": hop_series
- },
-+ "csnDetails": csn_details,
- "metadata": {
- "totalServers": self._active_server_count or len(self.log_dirs),
- "configuredLogDirs": self.log_dirs,
---
-2.52.0
-
diff --git a/0017-Issue-7224-CI-Test-Simplify-test_reserve_descriptor_.patch b/0017-Issue-7224-CI-Test-Simplify-test_reserve_descriptor_.patch
deleted file mode 100644
index 35a391e..0000000
--- a/0017-Issue-7224-CI-Test-Simplify-test_reserve_descriptor_.patch
+++ /dev/null
@@ -1,93 +0,0 @@
-From de48dd0da38fc715783e8efaada5af4c22298b0e Mon Sep 17 00:00:00 2001
-From: James Chapman <jachapma@redhat.com>
-Date: Thu, 5 Feb 2026 15:33:08 +0000
-Subject: [PATCH] Issue 7224 - CI Test - Simplify
- test_reserve_descriptor_validation (#7225)
-
-Description:
-Previously, the test_reserve_descriptor_validation CItest calculated
-the expected number of file descriptors based on backends, indexes,
-SSL/FIPS mode, and compared it to the value returned by the server.
-This approach is fragile, especially in FIPS mode.
-
-Fix:
-The test has been updated to simply verify that the server corrects
-the configured nsslapd-reservedescriptors value if it is set too low,
-instead of calculating the expected total.
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7224
-
-Reviewed by: @bsimonova (Thank you)
----
- .../suites/resource_limits/fdlimits_test.py | 36 +++++++------------
- 1 file changed, 13 insertions(+), 23 deletions(-)
-
-diff --git a/dirsrvtests/tests/suites/resource_limits/fdlimits_test.py b/dirsrvtests/tests/suites/resource_limits/fdlimits_test.py
-index c843a4b24..a49e378c3 100644
---- a/dirsrvtests/tests/suites/resource_limits/fdlimits_test.py
-+++ b/dirsrvtests/tests/suites/resource_limits/fdlimits_test.py
-@@ -27,7 +27,7 @@ RESRV_FD_ATTR = "nsslapd-reservedescriptors"
- GLOBAL_LIMIT = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
- SYSTEMD_LIMIT = ensure_str(check_output("systemctl show -p LimitNOFILE dirsrv@standalone1".split(" ")).strip()).split('=')[1]
- CUSTOM_VAL = str(int(SYSTEMD_LIMIT) - 10)
--RESRV_DESC_VAL = str(10)
-+RESRV_DESC_VAL_LOW = 10
- TOO_HIGH_VAL = str(GLOBAL_LIMIT * 2)
- TOO_HIGH_VAL2 = str(int(SYSTEMD_LIMIT) * 2)
- TOO_LOW_VAL = "0"
-@@ -86,40 +86,30 @@ def test_reserve_descriptor_validation(topology_st):
- :id: 9bacdbcc-7754-4955-8a56-1d8c82bce274
- :setup: Standalone Instance
- :steps:
-- 1. Set attr nsslapd-reservedescriptors to a low value of RESRV_DESC_VAL (10)
-+ 1. Set attr nsslapd-reservedescriptors to a low value (10)
- 2. Verify low value has been set
- 3. Restart instance (On restart the reservedescriptor attr will be validated)
-- 4. Check updated value for nsslapd-reservedescriptors attr
-+ 4. Verify corrected value for nsslapd-reservedescriptors > low value
- :expectedresults:
- 1. Success
-- 2. A value of RESRV_DESC_VAL (10) is returned
-+ 2. A value of RESRV_DESC_VAL_LOW (10) is returned
- 3. Success
-- 4. A value of STANDALONE_INST_RESRV_DESCS (55) is returned
-+ 4. Corrected value for nsslapd-reservedescriptors > low value
- """
-
-- # Set nsslapd-reservedescriptors to a low value (RESRV_DESC_VAL:10)
-- topology_st.standalone.config.set(RESRV_FD_ATTR, RESRV_DESC_VAL)
-- resrv_fd = topology_st.standalone.config.get_attr_val_utf8(RESRV_FD_ATTR)
-- assert resrv_fd == RESRV_DESC_VAL
-+ # Set nsslapd-reservedescriptors to a low value (10)
-+ topology_st.standalone.config.set(RESRV_FD_ATTR, str(RESRV_DESC_VAL_LOW))
-+ resrv_fd = int(topology_st.standalone.config.get_attr_val_utf8(RESRV_FD_ATTR))
-+ assert resrv_fd == RESRV_DESC_VAL_LOW
-
- # An instance restart triggers a validation of the configured nsslapd-reservedescriptors attribute
- topology_st.standalone.restart()
-
-- """
-- A standalone instance contains a single backend with default indexes
-- so we only check these. TODO add tests for repl, chaining, PTA, SSL
-- """
-- STANDALONE_INST_RESRV_DESCS = 25 if is_fips() else 20 # Reserve descriptor constant (higher in FIPS mode)
-- backends = Backends(topology_st.standalone)
-- STANDALONE_INST_RESRV_DESCS += (len(backends.list()) * 4) # 4 = Backend descriptor constant
-- for be in backends.list() :
-- STANDALONE_INST_RESRV_DESCS += len(be.get_indexes().list())
--
-- # Varify reservedescriptors has been updated
-- resrv_fd = topology_st.standalone.config.get_attr_val_utf8(RESRV_FD_ATTR)
-- assert resrv_fd == str(STANDALONE_INST_RESRV_DESCS)
-+ # Get the corrected value
-+ corrected_fd = int(topology_st.standalone.config.get_attr_val_utf8(RESRV_FD_ATTR))
-+ assert corrected_fd > RESRV_DESC_VAL_LOW
-
-- log.info("test_reserve_descriptor_validation PASSED")
-+ log.info(f"test_reserve_descriptor_validation PASSED (corrected from {RESRV_DESC_VAL_LOW} to {corrected_fd})")
-
- @pytest.mark.skipif(ds_is_older("1.4.1.2"), reason="Not implemented")
- def test_reserve_descriptors_high(topology_st):
---
-2.52.0
-
diff --git a/0018-Issue-7223-Revert-index-scan-limits-for-system-index.patch b/0018-Issue-7223-Revert-index-scan-limits-for-system-index.patch
deleted file mode 100644
index 08c5af2..0000000
--- a/0018-Issue-7223-Revert-index-scan-limits-for-system-index.patch
+++ /dev/null
@@ -1,778 +0,0 @@
-From bdb2b85daa1358a182cf53bf68bbe019db5f82c3 Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Thu, 5 Feb 2026 12:17:06 +0100
-Subject: [PATCH] Issue 7223 - Revert index scan limits for system indexes
-
-This reverts changes introduced by the following commits:
-c6f458b42 Issue 7189 - DSBLE0007 generates incorrect remediation commands for scan limits
-8b6b3a9f9 Issue 6966 - On large DB, unlimited IDL scan limit reduce the SRCH performance
-
-Relates: https://github.com/389ds/389-ds-base/issues/7223
-
-Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
----
- .../tests/suites/config/config_test.py | 27 +---
- .../healthcheck/health_system_indexes_test.py | 136 +-----------------
- .../paged_results/paged_results_test.py | 25 +---
- ldap/servers/slapd/back-ldbm/back-ldbm.h | 1 -
- ldap/servers/slapd/back-ldbm/index.c | 2 -
- ldap/servers/slapd/back-ldbm/instance.c | 104 +++-----------
- ldap/servers/slapd/back-ldbm/ldbm_config.c | 30 ----
- ldap/servers/slapd/back-ldbm/ldbm_config.h | 1 -
- .../slapd/back-ldbm/ldbm_index_config.c | 8 --
- src/lib389/lib389/backend.py | 50 ++-----
- src/lib389/lib389/cli_conf/backend.py | 20 ---
- 11 files changed, 40 insertions(+), 364 deletions(-)
-
-diff --git a/dirsrvtests/tests/suites/config/config_test.py b/dirsrvtests/tests/suites/config/config_test.py
-index cbb8875fa..2c7d949d0 100644
---- a/dirsrvtests/tests/suites/config/config_test.py
-+++ b/dirsrvtests/tests/suites/config/config_test.py
-@@ -706,19 +706,17 @@ def test_ndn_cache_size_enforcement(topo, request):
-
- request.addfinalizer(fin)
-
--def test_require_index(topo, request):
-+def test_require_index(topo):
- """Validate that unindexed searches are rejected
-
- :id: fb6e31f2-acc2-4e75-a195-5c356faeb803
- :setup: Standalone instance
- :steps:
- 1. Set "nsslapd-require-index" to "on"
-- 2. ancestorid/idlscanlimit to 100
-- 3. Test an unindexed search is rejected
-+ 2. Test an unindexed search is rejected
- :expectedresults:
- 1. Success
- 2. Success
-- 3. Success
- """
-
- # Set the config
-@@ -729,10 +727,6 @@ def test_require_index(topo, request):
-
- db_cfg = DatabaseConfig(topo.standalone)
- db_cfg.set([('nsslapd-idlistscanlimit', '100')])
-- backend = Backends(topo.standalone).get_backend(DEFAULT_SUFFIX)
-- ancestorid_index = backend.get_index('ancestorid')
-- ancestorid_index.replace("nsIndexIDListScanLimit", ensure_bytes("limit=100 type=eq flags=AND"))
-- topo.standalone.restart()
-
- users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
- for i in range(101):
-@@ -743,15 +737,10 @@ def test_require_index(topo, request):
- with pytest.raises(ldap.UNWILLING_TO_PERFORM):
- raw_objects.filter("(description=test*)")
-
-- def fin():
-- ancestorid_index.replace("nsIndexIDListScanLimit", ensure_bytes("limit=5000 type=eq flags=AND"))
--
-- request.addfinalizer(fin)
--
-
-
- @pytest.mark.skipif(ds_is_older('1.4.2'), reason="The config setting only exists in 1.4.2 and higher")
--def test_require_internal_index(topo, request):
-+def test_require_internal_index(topo):
- """Ensure internal operations require indexed attributes
-
- :id: 22b94f30-59e3-4f27-89a1-c4f4be036f7f
-@@ -782,10 +771,6 @@ def test_require_internal_index(topo, request):
- # Create a bunch of users
- db_cfg = DatabaseConfig(topo.standalone)
- db_cfg.set([('nsslapd-idlistscanlimit', '100')])
-- backend = Backends(topo.standalone).get_backend(DEFAULT_SUFFIX)
-- ancestorid_index = backend.get_index('ancestorid')
-- ancestorid_index.replace("nsIndexIDListScanLimit", ensure_bytes("limit=100 type=eq flags=AND"))
-- topo.standalone.restart()
- users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
- for i in range(102, 202):
- users.create_test_user(uid=i)
-@@ -810,12 +795,6 @@ def test_require_internal_index(topo, request):
- with pytest.raises(ldap.UNWILLING_TO_PERFORM):
- user.delete()
-
-- def fin():
-- ancestorid_index.replace("nsIndexIDListScanLimit", ensure_bytes("limit=5000 type=eq flags=AND"))
--
-- request.addfinalizer(fin)
--
--
-
- def get_pstack(pid):
- """Get a pstack of the pid."""
-diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-index 486fad44b..140845a33 100644
---- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-@@ -172,9 +172,7 @@ def test_missing_parentid(topology_st, log_buffering_enabled):
-
- log.info("Re-add the parentId index")
- backend = Backends(standalone).get("userRoot")
-- backend.add_index("parentid", ["eq"], matching_rules=["integerOrderingMatch"],
-- idlistscanlimit=['limit=5000 type=eq flags=AND'])
-- standalone.restart()
-+ backend.add_index("parentid", ["eq"], matching_rules=["integerOrderingMatch"])
-
- run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
-@@ -263,8 +261,7 @@ def test_usn_plugin_missing_entryusn(topology_st, usn_plugin_enabled, log_buffer
-
- log.info("Re-add the entryusn index")
- backend = Backends(standalone).get("userRoot")
-- backend.add_index("entryusn", ["eq"], matching_rules=["integerOrderingMatch"],
-- idlistscanlimit=['limit=5000 type=eq flags=AND'])
-+ backend.add_index("entryusn", ["eq"], matching_rules=["integerOrderingMatch"])
-
- run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
-@@ -408,132 +405,6 @@ def test_retrocl_plugin_missing_matching_rule(topology_st, retrocl_plugin_enable
- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
-
-
--def test_missing_scanlimit(topology_st, log_buffering_enabled):
-- """Check if healthcheck returns DSBLE0007 code when parentId index is missing scanlimit
--
-- :id: 40e1bf6a-2397-459b-bdf3-f787ca118b86
-- :setup: Standalone instance
-- :steps:
-- 1. Create DS instance
-- 2. Remove nsIndexIDListScanLimit from parentId index
-- 3. Use healthcheck without --json option
-- 4. Use healthcheck with --json option
-- 5. Verify the remediation command has properly quoted scanlimit
-- 6. Re-add the scanlimit
-- 7. Use healthcheck without --json option
-- 8. Use healthcheck with --json option
-- :expectedresults:
-- 1. Success
-- 2. Success
-- 3. healthcheck reports DSBLE0007 code and related details
-- 4. healthcheck reports DSBLE0007 code and related details
-- 5. The scanlimit value is quoted in the remediation command
-- 6. Success
-- 7. healthcheck reports no issues found
-- 8. healthcheck reports no issues found
-- """
--
-- RET_CODE = "DSBLE0007"
-- PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
-- SCANLIMIT_VALUE = "limit=5000 type=eq flags=AND"
--
-- standalone = topology_st.standalone
--
-- log.info("Remove nsIndexIDListScanLimit from parentId index")
-- parentid_index = Index(standalone, PARENTID_DN)
-- parentid_index.remove("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
--
-- run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=RET_CODE)
--
-- # Verify the remediation command has properly quoted scanlimit
-- args = FakeArgs()
-- args.instance = standalone.serverid
-- args.verbose = standalone.verbose
-- args.list_errors = False
-- args.list_checks = False
-- args.exclude_check = []
-- args.check = ["backends"]
-- args.dry_run = False
-- args.json = False
-- health_check_run(standalone, topology_st.logcap.log, args)
-- # Check that the scanlimit is quoted in the output
-- assert topology_st.logcap.contains('--add-scanlimit "limit=5000 type=eq flags=AND"')
-- log.info("Verified scanlimit is properly quoted in remediation command")
-- topology_st.logcap.flush()
--
-- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=RET_CODE)
--
-- log.info("Re-add the nsIndexIDListScanLimit")
-- parentid_index = Index(standalone, PARENTID_DN)
-- parentid_index.add("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
--
-- run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
-- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
--
--
--def test_missing_matching_rule_and_scanlimit(topology_st, log_buffering_enabled):
-- """Check if healthcheck generates a single combined command when both matching rule and scanlimit are missing
--
-- :id: af8214ad-5e4c-422a-8f74-3e99227551df
-- :setup: Standalone instance
-- :steps:
-- 1. Create DS instance
-- 2. Remove both integerOrderingMatch and nsIndexIDListScanLimit from parentId index
-- 3. Use healthcheck and verify a single combined command is generated
-- 4. Re-add the matching rule and scanlimit
-- 5. Use healthcheck without --json option
-- 6. Use healthcheck with --json option
-- :expectedresults:
-- 1. Success
-- 2. Success
-- 3. healthcheck reports DSBLE0007 and generates a single command with both --add-mr and --add-scanlimit
-- 4. Success
-- 5. healthcheck reports no issues found
-- 6. healthcheck reports no issues found
-- """
--
-- RET_CODE = "DSBLE0007"
-- PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
-- SCANLIMIT_VALUE = "limit=5000 type=eq flags=AND"
--
-- standalone = topology_st.standalone
--
-- log.info("Remove both integerOrderingMatch and nsIndexIDListScanLimit from parentId index")
-- parentid_index = Index(standalone, PARENTID_DN)
-- parentid_index.remove("nsMatchingRule", "integerOrderingMatch")
-- parentid_index.remove("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
--
-- # Run healthcheck and verify combined command
-- args = FakeArgs()
-- args.instance = standalone.serverid
-- args.verbose = standalone.verbose
-- args.list_errors = False
-- args.list_checks = False
-- args.exclude_check = []
-- args.check = ["backends"]
-- args.dry_run = False
-- args.json = False
-- health_check_run(standalone, topology_st.logcap.log, args)
--
-- # Verify DSBLE0007 is reported
-- assert topology_st.logcap.contains(RET_CODE)
-- log.info("healthcheck returned code: %s" % RET_CODE)
--
-- # Verify a single combined command is generated with both --add-mr and --add-scanlimit
-- assert topology_st.logcap.contains('--add-mr integerOrderingMatch --add-scanlimit "limit=5000 type=eq flags=AND"')
-- log.info("Verified combined command with both --add-mr and --add-scanlimit")
--
-- topology_st.logcap.flush()
--
-- log.info("Re-add the integerOrderingMatch matching rule and scanlimit")
-- parentid_index = Index(standalone, PARENTID_DN)
-- parentid_index.add("nsMatchingRule", "integerOrderingMatch")
-- parentid_index.add("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
--
-- run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
-- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
--
--
- def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
- """Check if healthcheck returns DSBLE0007 code when multiple system indexes are missing
-
-@@ -574,8 +445,7 @@ def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
-
- log.info("Re-add the missing system indexes")
- backend = Backends(standalone).get("userRoot")
-- backend.add_index("parentid", ["eq"], matching_rules=["integerOrderingMatch"],
-- idlistscanlimit=['limit=5000 type=eq flags=AND'])
-+ backend.add_index("parentid", ["eq"], matching_rules=["integerOrderingMatch"])
- backend.add_index("nsuniqueid", ["eq"])
- standalone.restart()
-
-diff --git a/dirsrvtests/tests/suites/paged_results/paged_results_test.py b/dirsrvtests/tests/suites/paged_results/paged_results_test.py
-index 61d6702da..1bb94b53a 100644
---- a/dirsrvtests/tests/suites/paged_results/paged_results_test.py
-+++ b/dirsrvtests/tests/suites/paged_results/paged_results_test.py
-@@ -306,19 +306,19 @@ def test_search_success(topology_st, create_user, page_size, users_num):
- del_users(users_list)
-
-
--@pytest.mark.parametrize("page_size,users_num,suffix,attr_name,attr_value,expected_err, restart", [
-+@pytest.mark.parametrize("page_size,users_num,suffix,attr_name,attr_value,expected_err", [
- (50, 200, 'cn=config,%s' % DN_LDBM, 'nsslapd-idlistscanlimit', '100',
-- ldap.UNWILLING_TO_PERFORM, True),
-+ ldap.UNWILLING_TO_PERFORM),
- (5, 15, DN_CONFIG, 'nsslapd-timelimit', '20',
-- ldap.UNAVAILABLE_CRITICAL_EXTENSION, False),
-+ ldap.UNAVAILABLE_CRITICAL_EXTENSION),
- (21, 50, DN_CONFIG, 'nsslapd-sizelimit', '20',
-- ldap.SIZELIMIT_EXCEEDED, False),
-+ ldap.SIZELIMIT_EXCEEDED),
- (21, 50, DN_CONFIG, 'nsslapd-pagedsizelimit', '5',
-- ldap.SIZELIMIT_EXCEEDED, False),
-+ ldap.SIZELIMIT_EXCEEDED),
- (5, 50, 'cn=config,%s' % DN_LDBM, 'nsslapd-lookthroughlimit', '20',
-- ldap.ADMINLIMIT_EXCEEDED, False)])
-+ ldap.ADMINLIMIT_EXCEEDED)])
- def test_search_limits_fail(topology_st, create_user, page_size, users_num,
-- suffix, attr_name, attr_value, expected_err, restart):
-+ suffix, attr_name, attr_value, expected_err):
- """Verify that search with a simple paged results control
- throws expected exceptoins when corresponding limits are
- exceeded.
-@@ -341,15 +341,6 @@ def test_search_limits_fail(topology_st, create_user, page_size, users_num,
-
- users_list = add_users(topology_st, users_num, DEFAULT_SUFFIX)
- attr_value_bck = change_conf_attr(topology_st, suffix, attr_name, attr_value)
-- ancestorid_index = None
-- if attr_name == 'nsslapd-idlistscanlimit':
-- backend = Backends(topology_st.standalone).get_backend(DEFAULT_SUFFIX)
-- ancestorid_index = backend.get_index('ancestorid')
-- ancestorid_index.replace("nsIndexIDListScanLimit", ensure_bytes("limit=100 type=eq flags=AND"))
--
-- if (restart):
-- log.info('Instance restarted')
-- topology_st.standalone.restart()
- conf_param_dict = {attr_name: attr_value}
- search_flt = r'(uid=test*)'
- searchreq_attrlist = ['dn', 'sn']
-@@ -402,8 +393,6 @@ def test_search_limits_fail(topology_st, create_user, page_size, users_num,
- else:
- break
- finally:
-- if ancestorid_index:
-- ancestorid_index.replace("nsIndexIDListScanLimit", ensure_bytes("limit=5000 type=eq flags=AND"))
- del_users(users_list)
- change_conf_attr(topology_st, suffix, attr_name, attr_value_bck)
-
-diff --git a/ldap/servers/slapd/back-ldbm/back-ldbm.h b/ldap/servers/slapd/back-ldbm/back-ldbm.h
-index b187c26bc..e23e7ff43 100644
---- a/ldap/servers/slapd/back-ldbm/back-ldbm.h
-+++ b/ldap/servers/slapd/back-ldbm/back-ldbm.h
-@@ -583,7 +583,6 @@ struct ldbminfo
- int li_mode;
- int li_lookthroughlimit;
- int li_allidsthreshold;
-- int li_system_allidsthreshold;
- char *li_directory;
- int li_reslimit_lookthrough_handle;
- uint64_t li_dbcachesize;
-diff --git a/ldap/servers/slapd/back-ldbm/index.c b/ldap/servers/slapd/back-ldbm/index.c
-index 0ab82948c..a5004be19 100644
---- a/ldap/servers/slapd/back-ldbm/index.c
-+++ b/ldap/servers/slapd/back-ldbm/index.c
-@@ -997,8 +997,6 @@ index_read_ext_allids(
- }
- if (pb) {
- slapi_pblock_get(pb, SLAPI_SEARCH_IS_AND, &is_and);
-- } else if (strcasecmp(type, LDBM_ANCESTORID_STR) == 0) {
-- is_and = 1;
- }
- ai_flags = is_and ? INDEX_ALLIDS_FLAG_AND : 0;
- /* the caller can pass in a value of 0 - just ignore those - but if the index
-diff --git a/ldap/servers/slapd/back-ldbm/instance.c b/ldap/servers/slapd/back-ldbm/instance.c
-index 2a6e8cbb8..2b71cd4f7 100644
---- a/ldap/servers/slapd/back-ldbm/instance.c
-+++ b/ldap/servers/slapd/back-ldbm/instance.c
-@@ -16,7 +16,7 @@
-
- /* Forward declarations */
- static void ldbm_instance_destructor(void **arg);
--Slapi_Entry *ldbm_instance_init_config_entry(char *cn_val, char *v1, char *v2, char *v3, char *v4, char *mr, char *scanlimit);
-+Slapi_Entry *ldbm_instance_init_config_entry(char *cn_val, char *v1, char *v2, char *v3, char *v4, char *mr);
-
-
- /* Creates and initializes a new ldbm_instance structure.
-@@ -126,7 +126,7 @@ done:
- * Take a bunch of strings, and create a index config entry
- */
- Slapi_Entry *
--ldbm_instance_init_config_entry(char *cn_val, char *val1, char *val2, char *val3, char *val4, char *mr, char *scanlimit)
-+ldbm_instance_init_config_entry(char *cn_val, char *val1, char *val2, char *val3, char *val4, char *mr)
- {
- Slapi_Entry *e = slapi_entry_alloc();
- struct berval *vals[2];
-@@ -167,11 +167,6 @@ ldbm_instance_init_config_entry(char *cn_val, char *val1, char *val2, char *val3
- slapi_entry_add_values(e, "nsMatchingRule", vals);
- }
-
-- if (scanlimit) {
-- val.bv_val = scanlimit;
-- val.bv_len = strlen(scanlimit);
-- slapi_entry_add_values(e, "nsIndexIDListScanLimit", vals);
-- }
- return e;
- }
-
-@@ -184,60 +179,8 @@ ldbm_instance_create_default_indexes(backend *be)
- {
- Slapi_Entry *e;
- ldbm_instance *inst = (ldbm_instance *)be->be_instance_info;
-- struct ldbminfo *li = (struct ldbminfo *)be->be_database->plg_private;
- /* write the dse file only on the final index */
- int flags = LDBM_INSTANCE_CONFIG_DONT_WRITE;
-- char *ancestorid_indexes_limit = NULL;
-- char *parentid_indexes_limit = NULL;
-- struct attrinfo *ai = NULL;
-- int index_already_configured = 0;
-- struct index_idlistsizeinfo *iter;
-- int cookie;
-- int limit;
--
-- ainfo_get(be, (char *)LDBM_ANCESTORID_STR, &ai);
-- if (ai && ai->ai_idlistinfo) {
-- iter = (struct index_idlistsizeinfo *)dl_get_first(ai->ai_idlistinfo, &cookie);
-- if (iter) {
-- limit = iter->ai_idlistsizelimit;
-- slapi_log_err(SLAPI_LOG_BACKLDBM, "ldbm_instance_create_default_indexes",
-- "set ancestorid limit to %d from attribute index\n",
-- limit);
-- } else {
-- limit = li->li_system_allidsthreshold;
-- slapi_log_err(SLAPI_LOG_BACKLDBM, "ldbm_instance_create_default_indexes",
-- "set ancestorid limit to %d from default (fail to read limit)\n",
-- limit);
-- }
-- ancestorid_indexes_limit = slapi_ch_smprintf("limit=%d type=eq flags=AND", limit);
-- } else {
-- ancestorid_indexes_limit = slapi_ch_smprintf("limit=%d type=eq flags=AND", li->li_system_allidsthreshold);
-- slapi_log_err(SLAPI_LOG_BACKLDBM, "ldbm_instance_create_default_indexes",
-- "set ancestorid limit to %d from default (no attribute or limit)\n",
-- li->li_system_allidsthreshold);
-- }
--
-- ainfo_get(be, (char *)LDBM_PARENTID_STR, &ai);
-- if (ai && ai->ai_idlistinfo) {
-- iter = (struct index_idlistsizeinfo *)dl_get_first(ai->ai_idlistinfo, &cookie);
-- if (iter) {
-- limit = iter->ai_idlistsizelimit;
-- slapi_log_err(SLAPI_LOG_BACKLDBM, "ldbm_instance_create_default_indexes",
-- "set parentid limit to %d from attribute index\n",
-- limit);
-- } else {
-- limit = li->li_system_allidsthreshold;
-- slapi_log_err(SLAPI_LOG_BACKLDBM, "ldbm_instance_create_default_indexes",
-- "set parentid limit to %d from default (fail to read limit)\n",
-- limit);
-- }
-- parentid_indexes_limit = slapi_ch_smprintf("limit=%d type=eq flags=AND", limit);
-- } else {
-- parentid_indexes_limit = slapi_ch_smprintf("limit=%d type=eq flags=AND", li->li_system_allidsthreshold);
-- slapi_log_err(SLAPI_LOG_BACKLDBM, "ldbm_instance_create_default_indexes",
-- "set parentid limit to %d from default (no attribute or limit)\n",
-- li->li_system_allidsthreshold);
-- }
-
- /*
- * Always index (entrydn or entryrdn), parentid, objectclass,
-@@ -245,48 +188,42 @@ ldbm_instance_create_default_indexes(backend *be)
- * since they are used by some searches, replication and the
- * ACL routines.
- */
-- e = ldbm_instance_init_config_entry(LDBM_ENTRYRDN_STR, "subtree", 0, 0, 0, 0, 0);
-+ e = ldbm_instance_init_config_entry(LDBM_ENTRYRDN_STR, "subtree", 0, 0, 0, 0);
- ldbm_instance_config_add_index_entry(inst, e, flags);
- slapi_entry_free(e);
-
-- ainfo_get(be, (char *)LDBM_PARENTID_STR, &ai);
-- /* Check if the attrinfo is actually for parentid, not a fallback to .default */
-- index_already_configured = (ai != NULL && strcmp(ai->ai_type, LDBM_PARENTID_STR) == 0);
-- if (!index_already_configured) {
-- e = ldbm_instance_init_config_entry(LDBM_PARENTID_STR, "eq", 0, 0, 0, "integerOrderingMatch", parentid_indexes_limit);
-- ldbm_instance_config_add_index_entry(inst, e, flags);
-- attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
-- slapi_entry_free(e);
-- }
-+ e = ldbm_instance_init_config_entry(LDBM_PARENTID_STR, "eq", 0, 0, 0, "integerOrderingMatch");
-+ ldbm_instance_config_add_index_entry(inst, e, flags);
-+ slapi_entry_free(e);
-
-- e = ldbm_instance_init_config_entry("objectclass", "eq", 0, 0, 0, 0, 0);
-+ e = ldbm_instance_init_config_entry("objectclass", "eq", 0, 0, 0, 0);
- ldbm_instance_config_add_index_entry(inst, e, flags);
- slapi_entry_free(e);
-
-- e = ldbm_instance_init_config_entry("aci", "pres", 0, 0, 0, 0, 0);
-+ e = ldbm_instance_init_config_entry("aci", "pres", 0, 0, 0, 0);
- ldbm_instance_config_add_index_entry(inst, e, flags);
- slapi_entry_free(e);
-
-- e = ldbm_instance_init_config_entry(LDBM_NUMSUBORDINATES_STR, "pres", 0, 0, 0, 0, 0);
-+ e = ldbm_instance_init_config_entry(LDBM_NUMSUBORDINATES_STR, "pres", 0, 0, 0, 0);
- ldbm_instance_config_add_index_entry(inst, e, flags);
- slapi_entry_free(e);
-
-- e = ldbm_instance_init_config_entry(SLAPI_ATTR_UNIQUEID, "eq", 0, 0, 0, 0, 0);
-+ e = ldbm_instance_init_config_entry(SLAPI_ATTR_UNIQUEID, "eq", 0, 0, 0, 0);
- ldbm_instance_config_add_index_entry(inst, e, flags);
- slapi_entry_free(e);
-
- /* For MMR, we need this attribute (to replace use of dncomp in delete). */
-- e = ldbm_instance_init_config_entry(ATTR_NSDS5_REPLCONFLICT, "eq", "pres", 0, 0, 0, 0);
-+ e = ldbm_instance_init_config_entry(ATTR_NSDS5_REPLCONFLICT, "eq", "pres", 0, 0, 0);
- ldbm_instance_config_add_index_entry(inst, e, flags);
- slapi_entry_free(e);
-
- /* write the dse file only on the final index */
-- e = ldbm_instance_init_config_entry(SLAPI_ATTR_NSCP_ENTRYDN, "eq", 0, 0, 0, 0, 0);
-+ e = ldbm_instance_init_config_entry(SLAPI_ATTR_NSCP_ENTRYDN, "eq", 0, 0, 0, 0);
- ldbm_instance_config_add_index_entry(inst, e, flags);
- slapi_entry_free(e);
-
- /* ldbm_instance_config_add_index_entry(inst, 2, argv); */
-- e = ldbm_instance_init_config_entry(LDBM_PSEUDO_ATTR_DEFAULT, "none", 0, 0, 0, 0, 0);
-+ e = ldbm_instance_init_config_entry(LDBM_PSEUDO_ATTR_DEFAULT, "none", 0, 0, 0, 0);
- attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
- slapi_entry_free(e);
-
-@@ -294,18 +231,9 @@ ldbm_instance_create_default_indexes(backend *be)
- * ancestorid is special, there is actually no such attr type
- * but we still want to use the attr index file APIs.
- */
-- ainfo_get(be, (char *)LDBM_ANCESTORID_STR, &ai);
-- /* Check if the attrinfo is actually for ancestorid, not a fallback to .default */
-- index_already_configured = (ai != NULL && strcmp(ai->ai_type, LDBM_ANCESTORID_STR) == 0);
-- if (!index_already_configured) {
-- e = ldbm_instance_init_config_entry(LDBM_ANCESTORID_STR, "eq", 0, 0, 0, "integerOrderingMatch", ancestorid_indexes_limit);
-- ldbm_instance_config_add_index_entry(inst, e, flags);
-- attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
-- slapi_entry_free(e);
-- }
--
-- slapi_ch_free_string(&ancestorid_indexes_limit);
-- slapi_ch_free_string(&parentid_indexes_limit);
-+ e = ldbm_instance_init_config_entry(LDBM_ANCESTORID_STR, "eq", 0, 0, 0, "integerOrderingMatch");
-+ attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
-+ slapi_entry_free(e);
-
- return 0;
- }
-diff --git a/ldap/servers/slapd/back-ldbm/ldbm_config.c b/ldap/servers/slapd/back-ldbm/ldbm_config.c
-index c24e3d766..6a2ce4c27 100644
---- a/ldap/servers/slapd/back-ldbm/ldbm_config.c
-+++ b/ldap/servers/slapd/back-ldbm/ldbm_config.c
-@@ -385,35 +385,6 @@ ldbm_config_allidsthreshold_set(void *arg, void *value, char *errorbuf __attribu
- return retval;
- }
-
--static void *
--ldbm_config_system_allidsthreshold_get(void *arg)
--{
-- struct ldbminfo *li = (struct ldbminfo *)arg;
--
-- return (void *)((uintptr_t)(li->li_system_allidsthreshold));
--}
--
--static int
--ldbm_config_system_allidsthreshold_set(void *arg, void *value, char *errorbuf __attribute__((unused)), int phase __attribute__((unused)), int apply)
--{
-- struct ldbminfo *li = (struct ldbminfo *)arg;
-- int retval = LDAP_SUCCESS;
-- int val = (int)((uintptr_t)value);
--
-- /* Do whatever we can to make sure the data is ok. */
--
-- /* Catch attempts to configure a stupidly low ancestorid allidsthreshold */
-- if ((val > -1) && (val < 5000)) {
-- val = 5000;
-- }
--
-- if (apply) {
-- li->li_system_allidsthreshold = val;
-- }
--
-- return retval;
--}
--
- static void *
- ldbm_config_pagedallidsthreshold_get(void *arg)
- {
-@@ -1094,7 +1065,6 @@ static config_info ldbm_config[] = {
- {CONFIG_LOOKTHROUGHLIMIT, CONFIG_TYPE_INT, "5000", &ldbm_config_lookthroughlimit_get, &ldbm_config_lookthroughlimit_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
- {CONFIG_MODE, CONFIG_TYPE_INT_OCTAL, "0600", &ldbm_config_mode_get, &ldbm_config_mode_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
- {CONFIG_IDLISTSCANLIMIT, CONFIG_TYPE_INT, "2147483646", &ldbm_config_allidsthreshold_get, &ldbm_config_allidsthreshold_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
-- {CONFIG_SYSTEMIDLISTSCANLIMIT, CONFIG_TYPE_INT, "5000", &ldbm_config_system_allidsthreshold_get, &ldbm_config_system_allidsthreshold_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
- {CONFIG_DIRECTORY, CONFIG_TYPE_STRING, "", &ldbm_config_directory_get, &ldbm_config_directory_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE | CONFIG_FLAG_SKIP_DEFAULT_SETTING},
- {CONFIG_MAXPASSBEFOREMERGE, CONFIG_TYPE_INT, "100", &ldbm_config_maxpassbeforemerge_get, &ldbm_config_maxpassbeforemerge_set, 0},
-
-diff --git a/ldap/servers/slapd/back-ldbm/ldbm_config.h b/ldap/servers/slapd/back-ldbm/ldbm_config.h
-index 29a3426ab..e69bfeedf 100644
---- a/ldap/servers/slapd/back-ldbm/ldbm_config.h
-+++ b/ldap/servers/slapd/back-ldbm/ldbm_config.h
-@@ -60,7 +60,6 @@ struct config_info
- #define CONFIG_RANGELOOKTHROUGHLIMIT "nsslapd-rangelookthroughlimit"
- #define CONFIG_PAGEDLOOKTHROUGHLIMIT "nsslapd-pagedlookthroughlimit"
- #define CONFIG_IDLISTSCANLIMIT "nsslapd-idlistscanlimit"
--#define CONFIG_SYSTEMIDLISTSCANLIMIT "nsslapd-systemidlistscanlimit"
- #define CONFIG_PAGEDIDLISTSCANLIMIT "nsslapd-pagedidlistscanlimit"
- #define CONFIG_DIRECTORY "nsslapd-directory"
- #define CONFIG_MODE "nsslapd-mode"
-diff --git a/ldap/servers/slapd/back-ldbm/ldbm_index_config.c b/ldap/servers/slapd/back-ldbm/ldbm_index_config.c
-index bae2a64b9..38e7368e1 100644
---- a/ldap/servers/slapd/back-ldbm/ldbm_index_config.c
-+++ b/ldap/servers/slapd/back-ldbm/ldbm_index_config.c
-@@ -384,14 +384,6 @@ ldbm_instance_config_add_index_entry(
- }
- }
-
-- /* get nsIndexIDListScanLimit and its values, and add them */
-- if (0 == slapi_entry_attr_find(e, "nsIndexIDListScanLimit", &attr)) {
-- for (j = slapi_attr_first_value(attr, &sval); j != -1; j = slapi_attr_next_value(attr, j, &sval)) {
-- attrValue = slapi_value_get_berval(sval);
-- eBuf = PR_sprintf_append(eBuf, "nsIndexIDListScanLimit: %s\n", attrValue->bv_val);
-- }
-- }
--
- ldbm_config_add_dse_entry(li, eBuf, flags);
- if (eBuf) {
- PR_smprintf_free(eBuf);
-diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
-index 274d45abe..f3dbe7c92 100644
---- a/src/lib389/lib389/backend.py
-+++ b/src/lib389/lib389/backend.py
-@@ -645,10 +645,11 @@ class Backend(DSLdapObject):
- indexes = self.get_indexes()
-
- # Default system indexes taken from ldap/servers/slapd/back-ldbm/instance.c
-+ # Note: entryrdn and ancestorid are internal system indexes that are not
-+ # exposed in cn=config - they are managed internally by the server.
-+ # Only parentid has a DSE config entry (for the integerOrderingMatch rule).
- expected_system_indexes = {
-- 'entryrdn': {'types': ['subtree'], 'matching_rule': None},
-- 'parentid': {'types': ['eq'], 'matching_rule': 'integerOrderingMatch', 'scanlimit': 'limit=5000 type=eq flags=AND'},
-- 'ancestorid': {'types': ['eq'], 'matching_rule': 'integerOrderingMatch', 'scanlimit': 'limit=5000 type=eq flags=AND'},
-+ 'parentid': {'types': ['eq'], 'matching_rule': 'integerOrderingMatch'},
- 'objectClass': {'types': ['eq'], 'matching_rule': None},
- 'aci': {'types': ['pres'], 'matching_rule': None},
- 'nscpEntryDN': {'types': ['eq'], 'matching_rule': None},
-@@ -705,17 +706,14 @@ class Backend(DSLdapObject):
- # Generate remediation command
- index_types = ' '.join([f"--index-type {t}" for t in expected_config['types']])
- cmd = f"dsconf YOUR_INSTANCE backend index add {bename} --attr {attr_name} {index_types}"
-- if expected_config.get('matching_rule'):
-+ if expected_config['matching_rule']:
- cmd += f" --matching-rule {expected_config['matching_rule']}"
-- if expected_config.get('scanlimit'):
-- cmd += f" --add-scanlimit \"{expected_config['scanlimit']}\""
- remediation_commands.append(cmd)
- reindex_attrs.add(attr_name) # New index needs reindexing
- else:
- # Index exists, check configuration
- actual_types = index.get_attr_vals_utf8('nsIndexType') or []
- actual_mrs = index.get_attr_vals_utf8('nsMatchingRule') or []
-- actual_scanlimit = index.get_attr_vals_utf8('nsIndexIDListScanLimit') or []
-
- # Normalize to lowercase for comparison
- actual_types = [t.lower() for t in actual_types]
-@@ -730,31 +728,16 @@ class Backend(DSLdapObject):
- remediation_commands.append(cmd)
- reindex_attrs.add(attr_name)
-
-- # Check matching rules and scanlimit together to generate a single combined command
-+ # Check matching rules
- expected_mr = expected_config.get('matching_rule')
-- expected_scanlimit = expected_config.get('scanlimit')
--
-- missing_mr = False
- if expected_mr:
- actual_mrs_lower = [mr.lower() for mr in actual_mrs]
- if expected_mr.lower() not in actual_mrs_lower:
- discrepancies.append(f"Index {attr_name} missing matching rule: {expected_mr}")
-- missing_mr = True
--
-- missing_scanlimit = False
-- if expected_scanlimit and (len(actual_scanlimit) == 0):
-- discrepancies.append(f"Index {attr_name} missing fine grain definition of IDs limit: {expected_scanlimit}")
-- missing_scanlimit = True
--
-- # Generate a single combined command for all missing items
-- if missing_mr or missing_scanlimit:
-- cmd = f"dsconf YOUR_INSTANCE backend index set {bename} --attr {attr_name}"
-- if missing_mr:
-- cmd += f" --add-mr {expected_mr}"
-- if missing_scanlimit:
-- cmd += f" --add-scanlimit \"{expected_scanlimit}\""
-- remediation_commands.append(cmd)
-- reindex_attrs.add(attr_name)
-+ # Add the missing matching rule
-+ cmd = f"dsconf YOUR_INSTANCE backend index set {bename} --attr {attr_name} --add-mr {expected_mr}"
-+ remediation_commands.append(cmd)
-+ reindex_attrs.add(attr_name)
-
- except Exception as e:
- self._log.debug(f"_lint_system_indexes - Error checking index {attr_name}: {e}")
-@@ -993,13 +976,12 @@ class Backend(DSLdapObject):
- return
- raise ValueError("Can not delete index because it does not exist")
-
-- def add_index(self, attr_name, types, matching_rules=None, idlistscanlimit=None, reindex=False):
-+ def add_index(self, attr_name, types, matching_rules=None, reindex=False):
- """ Add an index.
-
- :param attr_name - name of the attribute to index
- :param types - a List of index types(eq, pres, sub, approx)
- :param matching_rules - a List of matching rules for the index
-- :param idlistscanlimit - a List of fine grain definitions for scanning limit
- :param reindex - If set to True then index the attribute after creating it.
- """
-
-@@ -1029,15 +1011,6 @@ class Backend(DSLdapObject):
- # Only add if there are actually rules present in the list.
- if len(mrs) > 0:
- props['nsMatchingRule'] = mrs
--
-- if idlistscanlimit is not None:
-- scanlimits = []
-- for scanlimit in idlistscanlimit:
-- scanlimits.append(scanlimit)
-- # Only add if there are actually limits in the list.
-- if len(scanlimits) > 0:
-- props['nsIndexIDListScanLimit'] = scanlimits
--
- new_index.create(properties=props, basedn="cn=index," + self._dn)
-
- if reindex:
-@@ -1349,7 +1322,6 @@ class DatabaseConfig(DSLdapObject):
- 'nsslapd-lookthroughlimit',
- 'nsslapd-mode',
- 'nsslapd-idlistscanlimit',
-- 'nsslapd-systemidlistscanlimit',
- 'nsslapd-directory',
- 'nsslapd-import-cachesize',
- 'nsslapd-idl-switch',
-diff --git a/src/lib389/lib389/cli_conf/backend.py b/src/lib389/lib389/cli_conf/backend.py
-index 9772e39d4..68efa795c 100644
---- a/src/lib389/lib389/cli_conf/backend.py
-+++ b/src/lib389/lib389/cli_conf/backend.py
-@@ -39,7 +39,6 @@ arg_to_attr = {
- 'mode': 'nsslapd-mode',
- 'state': 'nsslapd-state',
- 'idlistscanlimit': 'nsslapd-idlistscanlimit',
-- 'systemidlistscanlimit': 'nsslapd-systemidlistscanlimit',
- 'directory': 'nsslapd-directory',
- 'dbcachesize': 'nsslapd-dbcachesize',
- 'logdirectory': 'nsslapd-db-logdirectory',
-@@ -626,21 +625,6 @@ def backend_set_index(inst, basedn, log, args):
- except ldap.NO_SUCH_ATTRIBUTE:
- raise ValueError('Can not delete matching rule type because it does not exist')
-
-- if args.replace_scanlimit is not None:
-- for replace_scanlimit in args.replace_scanlimit:
-- index.replace('nsIndexIDListScanLimit', replace_scanlimit)
--
-- if args.add_scanlimit is not None:
-- for add_scanlimit in args.add_scanlimit:
-- index.add('nsIndexIDListScanLimit', add_scanlimit)
--
-- if args.del_scanlimit is not None:
-- for del_scanlimit in args.del_scanlimit:
-- try:
-- index.remove('nsIndexIDListScanLimit', del_scanlimit)
-- except ldap.NO_SUCH_ATTRIBUTE:
-- raise ValueError('Can not delete a fine grain limit definition because it does not exist')
--
- if args.reindex:
- be.reindex(attrs=[args.attr])
- log.info("Index successfully updated")
-@@ -963,9 +947,6 @@ def create_parser(subparsers):
- edit_index_parser.add_argument('--del-type', action='append', help='Removes an index type from the index: (eq, sub, pres, or approx)')
- edit_index_parser.add_argument('--add-mr', action='append', help='Adds a matching-rule to the index')
- edit_index_parser.add_argument('--del-mr', action='append', help='Removes a matching-rule from the index')
-- edit_index_parser.add_argument('--add-scanlimit', action='append', help='Adds a fine grain limit definiton to the index')
-- edit_index_parser.add_argument('--replace-scanlimit', action='append', help='Replaces a fine grain limit definiton to the index')
-- edit_index_parser.add_argument('--del-scanlimit', action='append', help='Removes a fine grain limit definiton to the index')
- edit_index_parser.add_argument('--reindex', action='store_true', help='Re-indexes the database after editing the index')
- edit_index_parser.add_argument('be_name', help='The backend name or suffix')
-
-@@ -1092,7 +1073,6 @@ def create_parser(subparsers):
- 'will check when examining candidate entries in response to a search request')
- set_db_config_parser.add_argument('--mode', help='Specifies the permissions used for newly created index files')
- set_db_config_parser.add_argument('--idlistscanlimit', help='Specifies the number of entry IDs that are searched during a search operation')
-- set_db_config_parser.add_argument('--systemidlistscanlimit', help='Specifies the number of entry IDs that are fetch from ancestorid/parentid indexes')
- set_db_config_parser.add_argument('--directory', help='Specifies absolute path to database instance')
- set_db_config_parser.add_argument('--dbcachesize', help='Specifies the database index cache size in bytes')
- set_db_config_parser.add_argument('--logdirectory', help='Specifies the path to the directory that contains the database transaction logs')
---
-2.52.0
-
diff --git a/0019-Issue-7223-Add-upgrade-function-to-remove-nsIndexIDL.patch b/0019-Issue-7223-Add-upgrade-function-to-remove-nsIndexIDL.patch
deleted file mode 100644
index 2efb8f5..0000000
--- a/0019-Issue-7223-Add-upgrade-function-to-remove-nsIndexIDL.patch
+++ /dev/null
@@ -1,212 +0,0 @@
-From ea1b51df9a698915b972923357caba1c78dcc82f Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Thu, 5 Feb 2026 12:17:06 +0100
-Subject: [PATCH] Issue 7223 - Add upgrade function to remove
- nsIndexIDListScanLimit from parentid
-
-Description:
-Add `upgrade_remove_index_scanlimit()` function that removes the
-nsIndexIDListScanLimit attribute from parentid index configuration
-if present.
-
-This attribute was incorrectly added by a previous version and can
-cause issues with index configuration. The upgrade function runs
-automatically on server startup and removes the attribute if found.
-
-Relates: https://github.com/389ds/389-ds-base/issues/7223
-
-Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
----
- .../healthcheck/health_system_indexes_test.py | 52 +++++++++
- ldap/servers/slapd/upgrade.c | 105 ++++++++++++++++++
- 2 files changed, 157 insertions(+)
-
-diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-index 140845a33..aea88e0e2 100644
---- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-@@ -453,6 +453,58 @@ def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
-
-
-+def test_upgrade_removes_parentid_scanlimit(topology_st):
-+ """Check if upgrade function removes nsIndexIDListScanLimit from parentid index
-+
-+ :id: 2808886e-c1c1-441d-b3a3-299c4ef1ab4a
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Use DSEldif to add nsIndexIDListScanLimit to parentid index
-+ 4. Start the server (triggers upgrade)
-+ 5. Verify nsIndexIDListScanLimit is removed from parentid index
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ 4. Success
-+ 5. nsIndexIDListScanLimit is no longer present
-+ """
-+ from lib389.dseldif import DSEldif
-+
-+ standalone = topology_st.standalone
-+ PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
-+ SCANLIMIT_VALUE = "limit=5000 type=eq flags=AND"
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Add nsIndexIDListScanLimit to parentid index using DSEldif")
-+ dse_ldif = DSEldif(standalone)
-+ dse_ldif.add(PARENTID_DN, "nsIndexIDListScanLimit", SCANLIMIT_VALUE)
-+
-+ # Verify it was added
-+ scanlimit = dse_ldif.get(PARENTID_DN, "nsIndexIDListScanLimit")
-+ assert scanlimit is not None, "Failed to add nsIndexIDListScanLimit"
-+ log.info(f"Added nsIndexIDListScanLimit: {scanlimit}")
-+
-+ log.info("Start the server (triggers upgrade)")
-+ standalone.start()
-+
-+ log.info("Verify nsIndexIDListScanLimit was removed by upgrade")
-+ # Check via LDAP - the upgrade should have removed it
-+ parentid_index = Index(standalone, PARENTID_DN)
-+ scanlimit_after = parentid_index.get_attr_vals_utf8("nsIndexIDListScanLimit")
-+ log.info(f"nsIndexIDListScanLimit after upgrade: {scanlimit_after}")
-+
-+ # The upgrade function should have removed nsIndexIDListScanLimit
-+ assert not scanlimit_after, \
-+ f"nsIndexIDListScanLimit should have been removed but found: {scanlimit_after}"
-+
-+ log.info("Upgrade successfully removed nsIndexIDListScanLimit from parentid index")
-+
-+
- if __name__ == "__main__":
- # Run isolated
- # -s for DEBUG mode
-diff --git a/ldap/servers/slapd/upgrade.c b/ldap/servers/slapd/upgrade.c
-index b02e37ed6..dcd16940b 100644
---- a/ldap/servers/slapd/upgrade.c
-+++ b/ldap/servers/slapd/upgrade.c
-@@ -330,6 +330,107 @@ upgrade_remove_subtree_rename(void)
- return UPGRADE_SUCCESS;
- }
-
-+/*
-+ * Remove nsIndexIDListScanLimit from parentid index configuration.
-+ *
-+ * This attribute was incorrectly added by a previous version and can
-+ * cause issues with index configuration. Remove it if present.
-+ */
-+static upgrade_status
-+upgrade_remove_index_scanlimit(void)
-+{
-+ struct slapi_pblock *pb = slapi_pblock_new();
-+ Slapi_Entry **backends = NULL;
-+ const char *be_base_dn = "cn=ldbm database,cn=plugins,cn=config";
-+ const char *be_filter = "(objectclass=nsBackendInstance)";
-+ const char *attrs_to_check[] = {"parentid", NULL};
-+ upgrade_status uresult = UPGRADE_SUCCESS;
-+
-+ /* Search for all backend instances */
-+ slapi_search_internal_set_pb(
-+ pb, be_base_dn,
-+ LDAP_SCOPE_ONELEVEL,
-+ be_filter, NULL, 0, NULL, NULL,
-+ plugin_get_default_component_id(), 0);
-+ slapi_search_internal_pb(pb);
-+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &backends);
-+
-+ if (backends) {
-+ for (size_t be_idx = 0; backends[be_idx] != NULL; be_idx++) {
-+ const char *be_dn = slapi_entry_get_dn_const(backends[be_idx]);
-+ const char *be_name = slapi_entry_attr_get_ref(backends[be_idx], "cn");
-+ if (!be_dn || !be_name) {
-+ continue;
-+ }
-+
-+ for (size_t attr_idx = 0; attrs_to_check[attr_idx] != NULL; attr_idx++) {
-+ const char *attr_name = attrs_to_check[attr_idx];
-+ struct slapi_pblock *idx_pb = slapi_pblock_new();
-+ Slapi_Entry **idx_entries = NULL;
-+ char *idx_dn = slapi_create_dn_string("cn=%s,cn=index,%s",
-+ attr_name, be_dn);
-+ char *idx_filter = "(objectclass=nsIndex)";
-+
-+ if (!idx_dn) {
-+ slapi_pblock_destroy(idx_pb);
-+ continue;
-+ }
-+
-+ slapi_search_internal_set_pb(
-+ idx_pb, idx_dn,
-+ LDAP_SCOPE_BASE,
-+ idx_filter, NULL, 0, NULL, NULL,
-+ plugin_get_default_component_id(), 0);
-+ slapi_search_internal_pb(idx_pb);
-+ slapi_pblock_get(idx_pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &idx_entries);
-+
-+ if (idx_entries && idx_entries[0]) {
-+ /* Check if nsIndexIDListScanLimit is present */
-+ if (slapi_entry_attr_get_ref(idx_entries[0], "nsIndexIDListScanLimit") != NULL) {
-+ /* Remove nsIndexIDListScanLimit */
-+ Slapi_PBlock *mod_pb = slapi_pblock_new();
-+ Slapi_Mods smods;
-+ int rc;
-+
-+ slapi_mods_init(&smods, 1);
-+ slapi_mods_add(&smods, LDAP_MOD_DELETE, "nsIndexIDListScanLimit", 0, NULL);
-+
-+ slapi_modify_internal_set_pb(
-+ mod_pb, idx_dn,
-+ slapi_mods_get_ldapmods_byref(&smods),
-+ NULL, NULL,
-+ plugin_get_default_component_id(), 0);
-+ slapi_modify_internal_pb(mod_pb);
-+ slapi_pblock_get(mod_pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
-+
-+ if (rc == LDAP_SUCCESS) {
-+ slapi_log_err(SLAPI_LOG_NOTICE, "upgrade_remove_index_scanlimit",
-+ "Removed 'nsIndexIDListScanLimit' from index '%s' in backend '%s'\n",
-+ attr_name, be_name);
-+ } else if (rc != LDAP_NO_SUCH_ATTRIBUTE) {
-+ slapi_log_err(SLAPI_LOG_ERR, "upgrade_remove_index_scanlimit",
-+ "Failed to remove 'nsIndexIDListScanLimit' from index '%s' in backend '%s': error %d\n",
-+ attr_name, be_name, rc);
-+ }
-+
-+ slapi_mods_done(&smods);
-+ slapi_pblock_destroy(mod_pb);
-+ }
-+ }
-+
-+ slapi_ch_free_string(&idx_dn);
-+ slapi_free_search_results_internal(idx_pb);
-+ slapi_pblock_destroy(idx_pb);
-+ }
-+ }
-+ }
-+
-+ slapi_free_search_results_internal(pb);
-+ slapi_pblock_destroy(pb);
-+
-+ return uresult;
-+}
-+
- /*
- * Check if parentid/ancestorid indexes are missing the integerOrderingMatch
- * matching rule.
-@@ -649,6 +750,10 @@ upgrade_server(void)
- return UPGRADE_FAILURE;
- }
-
-+ if (upgrade_remove_index_scanlimit() != UPGRADE_SUCCESS) {
-+ return UPGRADE_FAILURE;
-+ }
-+
- if (upgrade_check_id_index_matching_rule() != UPGRADE_SUCCESS) {
- return UPGRADE_FAILURE;
- }
---
-2.52.0
-
diff --git a/0020-Issue-7223-Add-upgrade-function-to-remove-ancestorid.patch b/0020-Issue-7223-Add-upgrade-function-to-remove-ancestorid.patch
deleted file mode 100644
index 3f989d7..0000000
--- a/0020-Issue-7223-Add-upgrade-function-to-remove-ancestorid.patch
+++ /dev/null
@@ -1,313 +0,0 @@
-From b7ca2adc43db1be935101d6eb5dbf06566ea43ed Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Thu, 5 Feb 2026 12:17:06 +0100
-Subject: [PATCH] Issue 7223 - Add upgrade function to remove ancestorid index
- config entry
-
-Description:
-Add `upgrade_remove_ancestorid_index_config()` function that removes:
-* ancestorid from `cn=default indexes`
-* ancestorid index config entries from each backend's `cn=index`
-
-Also remove ancestorid index configuration from template-dse.ldif.
-
-Relates: https://github.com/389ds/389-ds-base/issues/7223
-
-Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
----
- .../healthcheck/health_system_indexes_test.py | 85 +++++++++++
- ldap/ldif/template-dse.ldif.in | 8 --
- ldap/servers/slapd/upgrade.c | 133 +++++++++++++++++-
- 3 files changed, 214 insertions(+), 12 deletions(-)
-
-diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-index aea88e0e2..eb727b902 100644
---- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-@@ -504,6 +504,91 @@ def test_upgrade_removes_parentid_scanlimit(topology_st):
-
- log.info("Upgrade successfully removed nsIndexIDListScanLimit from parentid index")
-
-+ # Verify idempotency - restart again and ensure no errors
-+ log.info("Restart server again to verify idempotency (no errors on second run)")
-+ standalone.restart()
-+ # Verify the attribute is still absent
-+ scanlimit_after_second = parentid_index.get_attr_vals_utf8("nsIndexIDListScanLimit")
-+ assert not scanlimit_after_second, \
-+ f"nsIndexIDListScanLimit should still be absent after second restart but found: {scanlimit_after_second}"
-+ log.info("Idempotency verified - no issues on second restart")
-+
-+
-+def test_upgrade_removes_ancestorid_index_config(topology_st):
-+ """Check if upgrade function removes ancestorid index config entry
-+
-+ :id: 3f3d6e9b-75ac-4f0d-b2ce-7204e6eacd0a
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Use DSEldif to add an ancestorid index config entry
-+ 4. Start the server (triggers upgrade)
-+ 5. Verify ancestorid index config entry is removed
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ 4. Success
-+ 5. ancestorid index config entry is no longer present
-+ """
-+ from lib389.dseldif import DSEldif
-+
-+ standalone = topology_st.standalone
-+ ANCESTORID_DN = "cn=ancestorid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Add ancestorid index config entry using DSEldif")
-+ dse_ldif = DSEldif(standalone)
-+
-+ # Create a fake ancestorid index entry
-+ ancestorid_entry = [
-+ "dn: {}\n".format(ANCESTORID_DN),
-+ "objectClass: top\n",
-+ "objectClass: nsIndex\n",
-+ "cn: ancestorid\n",
-+ "nsSystemIndex: true\n",
-+ "nsIndexType: eq\n",
-+ "nsMatchingRule: integerOrderingMatch\n",
-+ "\n"
-+ ]
-+ dse_ldif.add_entry(ancestorid_entry)
-+
-+ # Verify it was added by re-reading dse.ldif
-+ dse_ldif2 = DSEldif(standalone)
-+ cn_value = dse_ldif2.get(ANCESTORID_DN, "cn")
-+ assert cn_value is not None, "Failed to add ancestorid index config entry"
-+ log.info(f"Added ancestorid index entry with cn: {cn_value}")
-+
-+ log.info("Start the server (triggers upgrade)")
-+ standalone.start()
-+
-+ log.info("Verify ancestorid index config entry was removed by upgrade")
-+ # Check via LDAP - the upgrade should have removed the entry
-+ try:
-+ ancestorid_index = Index(standalone, ANCESTORID_DN)
-+ # If we can get the entry, it wasn't removed - this is a failure
-+ cn_after = ancestorid_index.get_attr_vals_utf8("cn")
-+ assert False, f"ancestorid index config entry should have been removed but still exists: {cn_after}"
-+ except Exception as e:
-+ # Entry should not exist - this is expected
-+ log.info(f"ancestorid index config entry correctly removed (got exception: {e})")
-+
-+ log.info("Upgrade successfully removed ancestorid index config entry")
-+
-+ # Verify idempotency - restart again and ensure no errors
-+ log.info("Restart server again to verify idempotency (no errors on second run)")
-+ standalone.restart()
-+ # Verify the entry is still absent
-+ try:
-+ ancestorid_index = Index(standalone, ANCESTORID_DN)
-+ cn_after_second = ancestorid_index.get_attr_vals_utf8("cn")
-+ assert False, f"ancestorid index config entry should still be absent after second restart but found: {cn_after_second}"
-+ except Exception as e:
-+ log.info(f"Idempotency verified - ancestorid still absent after second restart (got exception: {e})")
-+
-
- if __name__ == "__main__":
- # Run isolated
-diff --git a/ldap/ldif/template-dse.ldif.in b/ldap/ldif/template-dse.ldif.in
-index bb8c71cd9..b6ab6f6c6 100644
---- a/ldap/ldif/template-dse.ldif.in
-+++ b/ldap/ldif/template-dse.ldif.in
-@@ -998,14 +998,6 @@ cn: aci
- nssystemindex: true
- nsindextype: pres
-
--dn: cn=ancestorid,cn=default indexes, cn=config,cn=ldbm database,cn=plugins,cn=config
--objectclass: top
--objectclass: nsIndex
--cn: ancestorid
--nssystemindex: true
--nsindextype: eq
--nsmatchingrule: integerOrderingMatch
--
- dn: cn=cn,cn=default indexes, cn=config,cn=ldbm database,cn=plugins,cn=config
- objectclass: top
- objectclass: nsIndex
-diff --git a/ldap/servers/slapd/upgrade.c b/ldap/servers/slapd/upgrade.c
-index dcd16940b..6b1b012da 100644
---- a/ldap/servers/slapd/upgrade.c
-+++ b/ldap/servers/slapd/upgrade.c
-@@ -431,6 +431,126 @@ upgrade_remove_index_scanlimit(void)
- return uresult;
- }
-
-+/*
-+ * Remove ancestorid index configuration entry if present.
-+ *
-+ * The ancestorid index is special - it has no corresponding attribute type
-+ * and should not have a DSE config entry. If an entry exists, remove it.
-+ *
-+ * This function removes:
-+ * 1. The ancestorid entry from cn=default indexes (to prevent re-creation on startup)
-+ * 2. The ancestorid entry from each backend's cn=index (if it exists)
-+ */
-+static upgrade_status
-+upgrade_remove_ancestorid_index_config(void)
-+{
-+ struct slapi_pblock *pb = slapi_pblock_new();
-+ Slapi_Entry **backends = NULL;
-+ const char *be_base_dn = "cn=ldbm database,cn=plugins,cn=config";
-+ const char *be_filter = "(objectclass=nsBackendInstance)";
-+ upgrade_status uresult = UPGRADE_SUCCESS;
-+ int rc;
-+
-+ /*
-+ * First, remove ancestorid from cn=default indexes to prevent
-+ * ldbm_instance_create_default_user_indexes() from re-creating it.
-+ */
-+ {
-+ Slapi_PBlock *def_pb = slapi_pblock_new();
-+ char *def_idx_dn = slapi_create_dn_string(
-+ "cn=ancestorid,cn=default indexes,cn=config,%s", be_base_dn);
-+
-+ if (def_idx_dn) {
-+ slapi_delete_internal_set_pb(
-+ def_pb, def_idx_dn, NULL, NULL,
-+ plugin_get_default_component_id(), 0);
-+ slapi_delete_internal_pb(def_pb);
-+ slapi_pblock_get(def_pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
-+
-+ if (rc == LDAP_SUCCESS) {
-+ slapi_log_err(SLAPI_LOG_NOTICE, "upgrade_remove_ancestorid_index_config",
-+ "Removed 'ancestorid' from default indexes.\n");
-+ } else if (rc != LDAP_NO_SUCH_OBJECT) {
-+ slapi_log_err(SLAPI_LOG_ERR, "upgrade_remove_ancestorid_index_config",
-+ "Failed to remove 'ancestorid' from default indexes: error %d\n", rc);
-+ }
-+
-+ slapi_ch_free_string(&def_idx_dn);
-+ }
-+ slapi_pblock_destroy(def_pb);
-+ }
-+
-+ /* Search for all backend instances */
-+ slapi_search_internal_set_pb(
-+ pb, be_base_dn,
-+ LDAP_SCOPE_ONELEVEL,
-+ be_filter, NULL, 0, NULL, NULL,
-+ plugin_get_default_component_id(), 0);
-+ slapi_search_internal_pb(pb);
-+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &backends);
-+
-+ if (backends) {
-+ for (size_t be_idx = 0; backends[be_idx] != NULL; be_idx++) {
-+ const char *be_dn = slapi_entry_get_dn_const(backends[be_idx]);
-+ const char *be_name = slapi_entry_attr_get_ref(backends[be_idx], "cn");
-+ if (!be_dn || !be_name) {
-+ continue;
-+ }
-+
-+ struct slapi_pblock *idx_pb = slapi_pblock_new();
-+ Slapi_Entry **idx_entries = NULL;
-+ char *idx_dn = slapi_create_dn_string("cn=ancestorid,cn=index,%s",
-+ be_dn);
-+ char *idx_filter = "(objectclass=nsIndex)";
-+
-+ if (!idx_dn) {
-+ slapi_pblock_destroy(idx_pb);
-+ continue;
-+ }
-+
-+ slapi_search_internal_set_pb(
-+ idx_pb, idx_dn,
-+ LDAP_SCOPE_BASE,
-+ idx_filter, NULL, 0, NULL, NULL,
-+ plugin_get_default_component_id(), 0);
-+ slapi_search_internal_pb(idx_pb);
-+ slapi_pblock_get(idx_pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &idx_entries);
-+
-+ if (idx_entries && idx_entries[0]) {
-+ /* ancestorid index entry exists - delete it */
-+ Slapi_PBlock *del_pb = slapi_pblock_new();
-+
-+ slapi_delete_internal_set_pb(
-+ del_pb, idx_dn, NULL, NULL,
-+ plugin_get_default_component_id(), 0);
-+ slapi_delete_internal_pb(del_pb);
-+ slapi_pblock_get(del_pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
-+
-+ if (rc == LDAP_SUCCESS) {
-+ slapi_log_err(SLAPI_LOG_NOTICE, "upgrade_remove_ancestorid_index_config",
-+ "Removed 'ancestorid' index config entry in backend '%s'.\n",
-+ be_name);
-+ } else if (rc != LDAP_NO_SUCH_OBJECT) {
-+ slapi_log_err(SLAPI_LOG_ERR, "upgrade_remove_ancestorid_index_config",
-+ "Failed to remove 'ancestorid' index config entry in backend '%s': error %d\n",
-+ be_name, rc);
-+ }
-+
-+ slapi_pblock_destroy(del_pb);
-+ }
-+
-+ slapi_ch_free_string(&idx_dn);
-+ slapi_free_search_results_internal(idx_pb);
-+ slapi_pblock_destroy(idx_pb);
-+ }
-+ }
-+
-+ slapi_free_search_results_internal(pb);
-+ slapi_pblock_destroy(pb);
-+
-+ return uresult;
-+}
-+
- /*
- * Check if parentid/ancestorid indexes are missing the integerOrderingMatch
- * matching rule.
-@@ -445,7 +565,7 @@ upgrade_check_id_index_matching_rule(void)
- Slapi_Entry **backends = NULL;
- const char *be_base_dn = "cn=ldbm database,cn=plugins,cn=config";
- const char *be_filter = "(objectclass=nsBackendInstance)";
-- const char *attrs_to_check[] = {"parentid", "ancestorid", NULL};
-+ const char *attrs_to_check[] = {"parentid", NULL};
- upgrade_status uresult = UPGRADE_SUCCESS;
-
- /* Search for all backend instances */
-@@ -459,8 +579,9 @@ upgrade_check_id_index_matching_rule(void)
-
- if (backends) {
- for (size_t be_idx = 0; backends[be_idx] != NULL; be_idx++) {
-+ const char *be_dn = slapi_entry_get_dn_const(backends[be_idx]);
- const char *be_name = slapi_entry_attr_get_ref(backends[be_idx], "cn");
-- if (!be_name) {
-+ if (!be_dn || !be_name) {
- continue;
- }
-
-@@ -469,8 +590,8 @@ upgrade_check_id_index_matching_rule(void)
- const char *attr_name = attrs_to_check[attr_idx];
- struct slapi_pblock *idx_pb = slapi_pblock_new();
- Slapi_Entry **idx_entries = NULL;
-- char *idx_dn = slapi_create_dn_string("cn=%s,cn=index,cn=%s,%s",
-- attr_name, be_name, be_base_dn);
-+ char *idx_dn = slapi_create_dn_string("cn=%s,cn=index,%s",
-+ attr_name, be_dn);
- char *idx_filter = "(objectclass=nsIndex)";
- PRBool has_matching_rule = PR_FALSE;
-
-@@ -754,6 +875,10 @@ upgrade_server(void)
- return UPGRADE_FAILURE;
- }
-
-+ if (upgrade_remove_ancestorid_index_config() != UPGRADE_SUCCESS) {
-+ return UPGRADE_FAILURE;
-+ }
-+
- if (upgrade_check_id_index_matching_rule() != UPGRADE_SUCCESS) {
- return UPGRADE_FAILURE;
- }
---
-2.52.0
-
diff --git a/0021-Issue-7223-Detect-and-log-index-ordering-mismatch-du.patch b/0021-Issue-7223-Detect-and-log-index-ordering-mismatch-du.patch
deleted file mode 100644
index bff21ab..0000000
--- a/0021-Issue-7223-Detect-and-log-index-ordering-mismatch-du.patch
+++ /dev/null
@@ -1,300 +0,0 @@
-From 61d74ec426fa9404bb6ed49ca4fe644f69ef32f2 Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Thu, 5 Feb 2026 12:17:06 +0100
-Subject: [PATCH] Issue 7223 - Detect and log index ordering mismatch during
- backend startup
-
-Description:
-Add `ldbm_instance_check_index_config()` function that checks on-disk
-index data and logs a message in case of a mismatch with DSE config entry.
-
-Relates: https://github.com/389ds/389-ds-base/issues/7223
-
-Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
----
- ldap/servers/slapd/back-ldbm/instance.c | 262 ++++++++++++++++++++++++
- 1 file changed, 262 insertions(+)
-
-diff --git a/ldap/servers/slapd/back-ldbm/instance.c b/ldap/servers/slapd/back-ldbm/instance.c
-index 2b71cd4f7..17bfc09a0 100644
---- a/ldap/servers/slapd/back-ldbm/instance.c
-+++ b/ldap/servers/slapd/back-ldbm/instance.c
-@@ -239,6 +239,266 @@ ldbm_instance_create_default_indexes(backend *be)
- }
-
-
-+/*
-+ * Check if an index has integerOrderingMatch configured in DSE.
-+ *
-+ * This function performs an internal LDAP search to check if the index
-+ * configuration entry has nsMatchingRule: integerOrderingMatch.
-+ *
-+ * Parameters:
-+ * inst_name - backend instance name (e.g., "userRoot")
-+ * index_name - name of the index to check (e.g., "parentid", "ancestorid")
-+ *
-+ * Returns:
-+ * PR_TRUE if integerOrderingMatch is configured
-+ * PR_FALSE if not configured or index entry doesn't exist
-+ */
-+static PRBool
-+ldbm_instance_index_has_int_order_in_dse(const char *inst_name, const char *index_name)
-+{
-+ Slapi_PBlock *pb = NULL;
-+ Slapi_Entry **entries = NULL;
-+ char *idx_dn = NULL;
-+ PRBool has_int_order = PR_FALSE;
-+
-+ idx_dn = slapi_create_dn_string("cn=%s,cn=index,cn=%s,cn=ldbm database,cn=plugins,cn=config",
-+ index_name, inst_name);
-+ if (idx_dn == NULL) {
-+ return PR_FALSE;
-+ }
-+
-+ pb = slapi_pblock_new();
-+ slapi_search_internal_set_pb(pb, idx_dn, LDAP_SCOPE_BASE,
-+ "(objectclass=nsIndex)", NULL, 0, NULL, NULL,
-+ plugin_get_default_component_id(), 0);
-+ slapi_search_internal_pb(pb);
-+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &entries);
-+
-+ if (entries && entries[0]) {
-+ Slapi_Attr *mr_attr = NULL;
-+ if (slapi_entry_attr_find(entries[0], "nsMatchingRule", &mr_attr) == 0) {
-+ Slapi_Value *sval = NULL;
-+ int idx;
-+ for (idx = slapi_attr_first_value(mr_attr, &sval);
-+ idx != -1;
-+ idx = slapi_attr_next_value(mr_attr, idx, &sval)) {
-+ const struct berval *bval = slapi_value_get_berval(sval);
-+ if (bval && bval->bv_val &&
-+ strcasecmp(bval->bv_val, "integerOrderingMatch") == 0) {
-+ has_int_order = PR_TRUE;
-+ break;
-+ }
-+ }
-+ }
-+ }
-+
-+ slapi_ch_free_string(&idx_dn);
-+ slapi_free_search_results_internal(pb);
-+ slapi_pblock_destroy(pb);
-+
-+ return has_int_order;
-+}
-+
-+/*
-+ * Check a system index for ordering mismatch between config and on-disk data.
-+ *
-+ * This function compares what's configured in DSE (nsMatchingRule) with
-+ * what's actually on disk. A mismatch can occur in two scenarios:
-+ * 1. Ordering rule is configured but disk has lexicographic order
-+ * (rule was added after index was created)
-+ * 2. No ordering rule configured but disk has integer order
-+ * (rule was removed after index was created with it)
-+ *
-+ * This function reads the first keys from the specified index and checks
-+ * if they are stored in lexicographic order (string: "1" < "10" < "2") or
-+ * integer order (numeric: "1" < "2" < "10").
-+ *
-+ * Parameters:
-+ * be - backend
-+ * index_name - name of the index to check (e.g., "parentid", "ancestorid")
-+ *
-+ */
-+static void
-+ldbm_instance_check_index_config(backend *be, const char *index_name)
-+{
-+ ldbm_instance *inst = (ldbm_instance *)be->be_instance_info;
-+ struct attrinfo *ai = NULL;
-+ dbi_db_t *db = NULL;
-+ dbi_cursor_t dbc = {0};
-+ dbi_val_t key = {0};
-+ dbi_val_t data = {0};
-+ int ret = 0;
-+ PRBool config_has_int_order = PR_FALSE;
-+ PRBool disk_has_int_order = PR_TRUE; /* Assume integer order until proven otherwise */
-+ ID prev_id = 0;
-+ int key_count = 0;
-+ PRBool first_key = PR_TRUE;
-+ PRBool found_ordering_evidence = PR_FALSE;
-+
-+ slapi_log_err(SLAPI_LOG_DEBUG, "ldbm_instance_check_index_config",
-+ "Backend '%s': checking %s index ordering...\n",
-+ inst->inst_name, index_name);
-+
-+ /* Check if integerOrderingMatch is configured in DSE */
-+ config_has_int_order = ldbm_instance_index_has_int_order_in_dse(inst->inst_name, index_name);
-+
-+ /* Get attrinfo for the index */
-+ ainfo_get(be, (char *)index_name, &ai);
-+ if (ai == NULL || strcmp(ai->ai_type, index_name) != 0) {
-+ /* No index config found */
-+ slapi_log_err(SLAPI_LOG_DEBUG, "ldbm_instance_check_index_config",
-+ "Backend '%s': no %s attrinfo found, skipping check\n",
-+ inst->inst_name, index_name);
-+ return;
-+ }
-+
-+ /* Open the index file */
-+ ret = dblayer_get_index_file(be, ai, &db, 0);
-+ if (ret != 0 || db == NULL) {
-+ /* Index file doesn't exist or can't be opened - this is fine for new instances */
-+ slapi_log_err(SLAPI_LOG_DEBUG, "ldbm_instance_check_index_config",
-+ "Backend '%s': could not open %s index file (ret=%d), skipping order check\n",
-+ inst->inst_name, index_name, ret);
-+ return;
-+ }
-+
-+ /* Create a cursor to read keys */
-+ ret = dblayer_new_cursor(be, db, NULL, &dbc);
-+ if (ret != 0) {
-+ slapi_log_err(SLAPI_LOG_ERR, "ldbm_instance_check_index_config",
-+ "Backend '%s': could not create cursor on %s index (ret=%d)\n",
-+ inst->inst_name, index_name, ret);
-+ dblayer_release_index_file(be, ai, db);
-+ return;
-+ }
-+
-+ dblayer_value_init(be, &key);
-+ dblayer_value_init(be, &data);
-+
-+ /*
-+ * Read up to 100 unique keys and check their ordering.
-+ * With lexicographic ordering: "1" < "10" < "100" < "2" < "20" < "3"
-+ * With integer ordering: "1" < "2" < "3" < "10" < "20" < "100"
-+ *
-+ * If we find a case where prev_id > current_id (numerically), but the
-+ * keys are still in order (lexicographically), then the index uses
-+ * lexicographic ordering.
-+ */
-+ while (key_count < 100) {
-+ ID current_id;
-+
-+ ret = dblayer_cursor_op(&dbc, first_key ? DBI_OP_MOVE_TO_FIRST : DBI_OP_NEXT_KEY, &key, &data);
-+ first_key = PR_FALSE; /* Always advance cursor on next iteration */
-+ if (ret != 0) {
-+ break; /* No more keys or error */
-+ }
-+
-+ /* Skip non-equality keys */
-+ if (key.size < 2 || *(char *)key.data != EQ_PREFIX) {
-+ continue;
-+ }
-+
-+ /* Parse the ID from the key (format: "=<id>") */
-+ current_id = (ID)strtoul((char *)key.data + 1, NULL, 10);
-+ if (current_id == 0) {
-+ continue; /* Invalid ID, skip */
-+ }
-+
-+ key_count++;
-+
-+ if (prev_id != 0) {
-+ /*
-+ * Check ordering: if prev_id > current_id numerically,
-+ * but we got this key after prev in DB order, then
-+ * the index is using lexicographic ordering.
-+ *
-+ * Example: if we see "10" followed by "2", that's lexicographic
-+ * because "10" < "2" as strings, but 10 > 2 as integers.
-+ */
-+ if (prev_id > current_id) {
-+ /* Found evidence of lexicographic ordering */
-+ disk_has_int_order = PR_FALSE;
-+ found_ordering_evidence = PR_TRUE;
-+ break;
-+ } else if (prev_id < current_id) {
-+ /*
-+ * This is consistent with integer ordering, but we need
-+ * to find a case that proves lexicographic ordering.
-+ * For example, seeing "1" followed by "2" is ambiguous,
-+ * but seeing "1" followed by "10" (not "2") proves lexicographic.
-+ *
-+ * A definitive test: if we see an ID followed by a smaller
-+ * ID, that's lexicographic. If all IDs are strictly increasing,
-+ * it could be either (or the index only has sequential IDs).
-+ */
-+ found_ordering_evidence = PR_TRUE;
-+ }
-+ }
-+ prev_id = current_id;
-+ }
-+
-+ /* Close the cursor and free values */
-+ dblayer_cursor_op(&dbc, DBI_OP_CLOSE, NULL, NULL);
-+ dblayer_value_free(be, &key);
-+ dblayer_value_free(be, &data);
-+
-+ /* Release the index file */
-+ dblayer_release_index_file(be, ai, db);
-+
-+ /*
-+ * Report findings and check for config/disk mismatch.
-+ * Log an error if there's a discrepancy between what's configured
-+ * in DSE and what's actually on disk.
-+ */
-+ if (!found_ordering_evidence) {
-+ slapi_log_err(SLAPI_LOG_DEBUG, "ldbm_instance_check_index_config",
-+ "Backend '%s': %s index ordering check - "
-+ "could not determine on-disk ordering (index may be empty or have sequential IDs only). "
-+ "Config has integerOrderingMatch: %s\n",
-+ inst->inst_name, index_name, config_has_int_order ? "yes" : "no");
-+ } else if (config_has_int_order && !disk_has_int_order) {
-+ /* Config expects integer ordering, but disk has lexicographic - MISMATCH */
-+ slapi_log_err(SLAPI_LOG_ERR, "ldbm_instance_check_index_config",
-+ "Backend '%s': MISMATCH - %s index has integerOrderingMatch configured, "
-+ "but on-disk data uses lexicographic ordering. "
-+ "This will cause searches to return incorrect or incomplete results. "
-+ "Please reindex the %s attribute: "
-+ "dsconf <instance> backend index reindex --attr %s %s\n",
-+ inst->inst_name, index_name, index_name, index_name, inst->inst_name);
-+ } else if (!config_has_int_order && disk_has_int_order) {
-+ /* Config expects lexicographic ordering, but disk has integer - MISMATCH */
-+ slapi_log_err(SLAPI_LOG_ERR, "ldbm_instance_check_index_config",
-+ "Backend '%s': MISMATCH - %s index does not have integerOrderingMatch configured, "
-+ "but on-disk data uses integer ordering. "
-+ "This will cause searches to return incorrect or incomplete results. "
-+ "Please reindex the %s attribute: "
-+ "dsconf <instance> backend index reindex --attr %s %s\n",
-+ inst->inst_name, index_name, index_name, index_name, inst->inst_name);
-+ } else {
-+ /* Config and disk ordering match - no action needed */
-+ slapi_log_err(SLAPI_LOG_DEBUG, "ldbm_instance_check_index_config",
-+ "Backend '%s': %s index ordering check passed - "
-+ "config has integerOrderingMatch: %s, on-disk data matches.\n",
-+ inst->inst_name, index_name, config_has_int_order ? "yes" : "no");
-+ }
-+}
-+
-+/*
-+ * Check system indexes for ordering mismatches.
-+ * If a mismatch is detected, log an error advising the administrator
-+ * to reindex the affected attribute.
-+ *
-+ * Note: We only check parentid here. The ancestorid index is a special
-+ * system index that has no DSE config entry - its ordering is hardcoded
-+ * in ldbm_instance_init_config_entry() and cannot be changed by users.
-+ */
-+static void
-+ldbm_instance_check_indexes(backend *be)
-+{
-+ /* Check parentid index */
-+ ldbm_instance_check_index_config(be, LDBM_PARENTID_STR);
-+}
-+
- /* Starts a backend instance */
- int
- ldbm_instance_start(backend *be)
-@@ -308,6 +568,8 @@ ldbm_instance_startall(struct ldbminfo *li)
- ldbm_instance_register_modify_callback(inst);
- vlv_init(inst);
- slapi_mtn_be_started(inst->inst_be);
-+ /* Check index configuration for potential issues */
-+ ldbm_instance_check_indexes(inst->inst_be);
- }
- if (slapi_exist_referral(inst->inst_be)) {
- slapi_be_set_flag(inst->inst_be, SLAPI_BE_FLAG_CONTAINS_REFERRAL);
---
-2.52.0
-
diff --git a/0022-Issue-7223-Add-dsctl-index-check-command-for-offline.patch b/0022-Issue-7223-Add-dsctl-index-check-command-for-offline.patch
deleted file mode 100644
index 818a9c7..0000000
--- a/0022-Issue-7223-Add-dsctl-index-check-command-for-offline.patch
+++ /dev/null
@@ -1,1233 +0,0 @@
-From d681f2e619caa2efddae83abc7d13860641a90cd Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Thu, 5 Feb 2026 12:17:06 +0100
-Subject: [PATCH] Issue 7223 - Add dsctl index-check command for offline index
- repair
-
-Description:
-Add `dsctl <instance> index-check [backend] [--fix]` command for offline
-detection and repair of index ordering mismatches. This is needed after
-upgrade from versions that didn't use integerOrderingMatch for
-parentid/ancestorid system indexes.
-
-It's automatically executed as part of RPM %post scriptlet during
-upgrade.
-
-Relates: https://github.com/389ds/389-ds-base/issues/7223
-
-Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
----
- .../healthcheck/health_system_indexes_test.py | 593 ++++++++++++++++++
- rpm/389-ds-base.spec.in | 51 +-
- src/lib389/lib389/cli_ctl/dbtasks.py | 402 ++++++++++++
- src/lib389/lib389/dseldif.py | 51 +-
- 4 files changed, 1068 insertions(+), 29 deletions(-)
-
-diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-index eb727b902..dd42cd197 100644
---- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-@@ -590,6 +590,599 @@ def test_upgrade_removes_ancestorid_index_config(topology_st):
- log.info(f"Idempotency verified - ancestorid still absent after second restart (got exception: {e})")
-
-
-+def test_index_check_basic(topology_st):
-+ """Check if dsctl index-check works correctly
-+
-+ :id: 8a4e5c2d-1f3b-4a7c-9e8d-2b6f0c4a5d3e
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Run dsctl index-check while server is running (should fail)
-+ 3. Stop the server
-+ 4. Run dsctl index-check (should pass)
-+ 5. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. index-check returns False and logs error
-+ 3. Success
-+ 4. index-check returns True (no mismatches)
-+ 5. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Run index-check while server is running")
-+ args = FakeArgs()
-+ args.backend = None
-+ args.fix = False
-+
-+ # Server should be running, index-check should fail
-+ assert standalone.status()
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is False
-+ assert topology_st.logcap.contains("index-check requires the instance to be stopped")
-+ topology_st.logcap.flush()
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Run index-check with server stopped")
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is True
-+ assert topology_st.logcap.contains("All checks passed")
-+ topology_st.logcap.flush()
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_specific_backend(topology_st):
-+ """Check if dsctl index-check works with a specific backend
-+
-+ :id: 407d8fcc-62e0-43dd-90fa-70e7090a5cfd
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Run dsctl index-check with specific backend (userRoot)
-+ 4. Run dsctl index-check with non-existent backend
-+ 5. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. index-check returns True for userRoot
-+ 4. index-check returns False for non-existent backend
-+ 5. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Run index-check for userRoot backend")
-+ args = FakeArgs()
-+ args.backend = "userRoot"
-+ args.fix = False
-+
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is True
-+ # Check for backend name in any case
-+ assert topology_st.logcap.contains("Checking backend:")
-+ topology_st.logcap.flush()
-+
-+ log.info("Run index-check for non-existent backend")
-+ args.backend = "nonExistentBackend"
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is False
-+ assert topology_st.logcap.contains("not found")
-+ topology_st.logcap.flush()
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_mismatch_detection(topology_st):
-+ """Check if dsctl index-check detects ordering mismatch
-+
-+ :id: 50d14520-b0bf-4243-9fe6-b097928d4351
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Run dsctl index-check (without --fix)
-+ 4. Verify output format
-+ 5. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. index-check returns True (no mismatch on fresh instance)
-+ 4. Log contains expected format
-+ 5. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Run index-check to verify detection logic")
-+ args = FakeArgs()
-+ args.backend = "userRoot"
-+ args.fix = False
-+
-+ # On a fresh instance, there should be no mismatch
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ # Fresh instance should have matching config and disk ordering
-+ assert result is True
-+ # Check that the backend was checked (may skip indexes if ordering can't be determined)
-+ assert topology_st.logcap.contains("Checking backend:")
-+ topology_st.logcap.flush()
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_with_fix(topology_st):
-+ """Check if dsctl index-check --fix triggers reindexing
-+
-+ :id: 38ae36e4-c861-4771-ae7d-354370376a2f
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Run dsctl index-check --fix (should pass since no mismatch)
-+ 4. Verify output indicates check passed
-+ 5. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. index-check returns True
-+ 4. Log contains "All checks passed"
-+ 5. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Run index-check with --fix option")
-+ args = FakeArgs()
-+ args.backend = None
-+ args.fix = True
-+
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ # On a fresh instance, there should be no mismatch, so no reindexing needed
-+ assert result is True
-+ assert topology_st.logcap.contains("All checks passed")
-+ topology_st.logcap.flush()
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_fixes_scanlimit(topology_st):
-+ """Check if dsctl index-check --fix removes nsIndexIDListScanLimit
-+
-+ :id: 4a9b2c7d-8e1f-4b3a-9c5d-6e7f8a0b1c2d
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Add nsIndexIDListScanLimit to parentid index using DSEldif
-+ 4. Run dsctl index-check (should detect issue)
-+ 5. Run dsctl index-check --fix
-+ 6. Verify nsIndexIDListScanLimit was removed
-+ 7. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ 4. index-check returns False and detects scanlimit
-+ 5. index-check returns True after fix
-+ 6. nsIndexIDListScanLimit no longer present
-+ 7. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+ from lib389.dseldif import DSEldif
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Add nsIndexIDListScanLimit to parentid index using DSEldif")
-+ dse_ldif = DSEldif(standalone)
-+ parentid_dn = "cn=parentid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config"
-+ dse_ldif.add(parentid_dn, "nsIndexIDListScanLimit", "4000")
-+
-+ # Verify it was added
-+ scanlimit = dse_ldif.get(parentid_dn, "nsIndexIDListScanLimit", single=True)
-+ assert scanlimit == "4000", f"Failed to add nsIndexIDListScanLimit, got: {scanlimit}"
-+ log.info("Added nsIndexIDListScanLimit to parentid index")
-+
-+ log.info("Run index-check without --fix (should detect issue)")
-+ args = FakeArgs()
-+ args.backend = "userRoot"
-+ args.fix = False
-+
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is False, "index-check should detect scanlimit issue"
-+ assert topology_st.logcap.contains("nsIndexIDListScanLimit")
-+ topology_st.logcap.flush()
-+
-+ log.info("Run index-check with --fix")
-+ args.fix = True
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is True, "index-check --fix should succeed"
-+ assert topology_st.logcap.contains("Removed nsIndexIDListScanLimit")
-+ topology_st.logcap.flush()
-+
-+ log.info("Verify nsIndexIDListScanLimit was removed")
-+ dse_ldif = DSEldif(standalone) # Reload to get fresh data
-+ scanlimit = dse_ldif.get(parentid_dn, "nsIndexIDListScanLimit", single=True)
-+ assert scanlimit is None, f"nsIndexIDListScanLimit should be removed, but got: {scanlimit}"
-+ log.info("nsIndexIDListScanLimit successfully removed")
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_fixes_ancestorid_config(topology_st):
-+ """Check if dsctl index-check --fix removes ancestorid config entries
-+
-+ :id: 5b0c3d8e-9f2a-4c4b-0d6e-7f8a9b1c2d3e
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Add ancestorid index config entry using DSEldif
-+ 4. Run dsctl index-check (should detect issue)
-+ 5. Run dsctl index-check --fix
-+ 6. Verify ancestorid config entry was removed
-+ 7. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ 4. index-check returns False and detects ancestorid config
-+ 5. index-check returns True after fix
-+ 6. ancestorid config entry no longer present
-+ 7. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+ from lib389.dseldif import DSEldif
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Add ancestorid index config entry using DSEldif")
-+ dse_ldif = DSEldif(standalone)
-+ ancestorid_entry = [
-+ "dn: cn=ancestorid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config\n",
-+ "objectClass: top\n",
-+ "objectClass: nsIndex\n",
-+ "cn: ancestorid\n",
-+ "nsSystemIndex: true\n",
-+ "nsIndexType: eq\n",
-+ ]
-+ dse_ldif.add_entry(ancestorid_entry)
-+
-+ # Verify it was added
-+ ancestorid_dn = "cn=ancestorid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config"
-+ dse_ldif = DSEldif(standalone) # Reload
-+ cn_value = dse_ldif.get(ancestorid_dn, "cn", single=True)
-+ assert cn_value is not None, "Failed to add ancestorid index config entry"
-+ log.info(f"Added ancestorid index entry with cn: {cn_value}")
-+
-+ log.info("Run index-check without --fix (should detect issue)")
-+ args = FakeArgs()
-+ args.backend = "userRoot"
-+ args.fix = False
-+
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is False, "index-check should detect ancestorid config issue"
-+ assert topology_st.logcap.contains("ancestorid") and topology_st.logcap.contains("config entry exists")
-+ topology_st.logcap.flush()
-+
-+ log.info("Run index-check with --fix")
-+ args.fix = True
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is True, "index-check --fix should succeed"
-+ assert topology_st.logcap.contains("Removed ancestorid config entry")
-+ topology_st.logcap.flush()
-+
-+ log.info("Verify ancestorid config entry was removed")
-+ dse_ldif = DSEldif(standalone) # Reload to get fresh data
-+ cn_value = dse_ldif.get(ancestorid_dn, "cn", single=True)
-+ assert cn_value is None, f"ancestorid config entry should be removed, but got: {cn_value}"
-+ log.info("ancestorid config entry successfully removed")
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_fixes_missing_matching_rule(topology_st):
-+ """Check if dsctl index-check --fix adds missing integerOrderingMatch
-+
-+ :id: 6c1d4e9f-0a3b-4d5c-1e7f-8a9b0c2d3e4f
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Remove integerOrderingMatch from parentid index using DSEldif
-+ 4. Run dsctl index-check (should detect issue)
-+ 5. Run dsctl index-check --fix
-+ 6. Verify integerOrderingMatch was added back
-+ 7. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ 4. index-check returns False and detects missing matching rule
-+ 5. index-check returns True after fix
-+ 6. integerOrderingMatch is present
-+ 7. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+ from lib389.dseldif import DSEldif
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Remove integerOrderingMatch from parentid index using DSEldif")
-+ dse_ldif = DSEldif(standalone)
-+ parentid_dn = "cn=parentid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config"
-+
-+ # Check current matching rules
-+ matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
-+ log.info(f"Current matching rules: {matching_rules}")
-+
-+ # Remove integerOrderingMatch if present
-+ if matching_rules:
-+ for mr in matching_rules:
-+ if "integerorderingmatch" in mr.lower():
-+ dse_ldif.delete(parentid_dn, "nsMatchingRule", mr)
-+ log.info(f"Removed matching rule: {mr}")
-+
-+ # Verify it was removed
-+ dse_ldif = DSEldif(standalone) # Reload
-+ matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
-+ if matching_rules:
-+ for mr in matching_rules:
-+ assert "integerorderingmatch" not in mr.lower(), \
-+ f"integerOrderingMatch should be removed, but found: {mr}"
-+ log.info("integerOrderingMatch removed from parentid index")
-+
-+ log.info("Run index-check without --fix (should detect issue)")
-+ args = FakeArgs()
-+ args.backend = "userRoot"
-+ args.fix = False
-+
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is False, "index-check should detect missing matching rule"
-+ assert topology_st.logcap.contains("missing integerOrderingMatch")
-+ topology_st.logcap.flush()
-+
-+ log.info("Run index-check with --fix")
-+ args.fix = True
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is True, "index-check --fix should succeed"
-+ assert topology_st.logcap.contains("integerOrderingMatch")
-+ topology_st.logcap.flush()
-+
-+ log.info("Verify integerOrderingMatch was added back")
-+ dse_ldif = DSEldif(standalone) # Reload to get fresh data
-+ matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
-+ assert matching_rules is not None, "nsMatchingRule should be present"
-+ found_int_order = False
-+ for mr in matching_rules:
-+ if "integerorderingmatch" in mr.lower():
-+ found_int_order = True
-+ break
-+ assert found_int_order, f"integerOrderingMatch should be present, got: {matching_rules}"
-+ log.info("integerOrderingMatch successfully added back")
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_fixes_default_ancestorid(topology_st):
-+ """Check if dsctl index-check --fix removes ancestorid from default indexes
-+
-+ :id: 7d2e5f0a-1b4c-4e6d-2f8a-9b0c1d3e4f5a
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Add ancestorid to cn=default indexes using DSEldif
-+ 4. Run dsctl index-check (should detect issue)
-+ 5. Run dsctl index-check --fix
-+ 6. Verify ancestorid was removed from default indexes
-+ 7. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ 4. index-check returns False and detects ancestorid in default indexes
-+ 5. index-check returns True after fix
-+ 6. ancestorid no longer in default indexes
-+ 7. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+ from lib389.dseldif import DSEldif
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Add ancestorid to cn=default indexes using DSEldif")
-+ dse_ldif = DSEldif(standalone)
-+ ancestorid_default_entry = [
-+ "dn: cn=ancestorid,cn=default indexes,cn=config,cn=ldbm database,cn=plugins,cn=config\n",
-+ "objectClass: top\n",
-+ "objectClass: nsIndex\n",
-+ "cn: ancestorid\n",
-+ "nsSystemIndex: true\n",
-+ "nsIndexType: eq\n",
-+ ]
-+ dse_ldif.add_entry(ancestorid_default_entry)
-+
-+ # Verify it was added
-+ ancestorid_default_dn = "cn=ancestorid,cn=default indexes,cn=config,cn=ldbm database,cn=plugins,cn=config"
-+ dse_ldif = DSEldif(standalone) # Reload
-+ cn_value = dse_ldif.get(ancestorid_default_dn, "cn", single=True)
-+ assert cn_value is not None, "Failed to add ancestorid to default indexes"
-+ log.info(f"Added ancestorid to default indexes with cn: {cn_value}")
-+
-+ log.info("Run index-check without --fix (should detect issue)")
-+ args = FakeArgs()
-+ args.backend = None # Check all backends including default indexes
-+ args.fix = False
-+
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is False, "index-check should detect ancestorid in default indexes"
-+ assert topology_st.logcap.contains("ancestorid found in cn=default indexes")
-+ topology_st.logcap.flush()
-+
-+ log.info("Run index-check with --fix")
-+ args.fix = True
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is True, "index-check --fix should succeed"
-+ assert topology_st.logcap.contains("Removed ancestorid from default indexes")
-+ topology_st.logcap.flush()
-+
-+ log.info("Verify ancestorid was removed from default indexes")
-+ dse_ldif = DSEldif(standalone) # Reload to get fresh data
-+ cn_value = dse_ldif.get(ancestorid_default_dn, "cn", single=True)
-+ assert cn_value is None, f"ancestorid should be removed from default indexes, but got: {cn_value}"
-+ log.info("ancestorid successfully removed from default indexes")
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_fixes_multiple_issues(topology_st):
-+ """Check if dsctl index-check --fix handles multiple issues at once
-+
-+ :id: 8e3f6a1b-2c5d-4f7e-3a9b-0c1d2e4f5a6b
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Add multiple issues: scanlimit, ancestorid config, missing matching rule
-+ 4. Run dsctl index-check (should detect all issues)
-+ 5. Run dsctl index-check --fix
-+ 6. Verify all issues were fixed
-+ 7. Run dsctl index-check again (should pass)
-+ 8. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ 4. index-check returns False and detects all issues
-+ 5. index-check returns True after fix
-+ 6. All issues resolved
-+ 7. index-check returns True (no issues)
-+ 8. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+ from lib389.dseldif import DSEldif
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ dse_ldif = DSEldif(standalone)
-+ parentid_dn = "cn=parentid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config"
-+ ancestorid_dn = "cn=ancestorid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config"
-+
-+ log.info("Add issue 1: nsIndexIDListScanLimit to parentid")
-+ dse_ldif.add(parentid_dn, "nsIndexIDListScanLimit", "4000")
-+
-+ log.info("Add issue 2: ancestorid index config entry")
-+ ancestorid_entry = [
-+ f"dn: {ancestorid_dn}\n",
-+ "objectClass: top\n",
-+ "objectClass: nsIndex\n",
-+ "cn: ancestorid\n",
-+ "nsSystemIndex: true\n",
-+ "nsIndexType: eq\n",
-+ ]
-+ dse_ldif.add_entry(ancestorid_entry)
-+
-+ log.info("Add issue 3: Remove integerOrderingMatch from parentid")
-+ dse_ldif = DSEldif(standalone) # Reload
-+ matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
-+ if matching_rules:
-+ for mr in matching_rules:
-+ if "integerorderingmatch" in mr.lower():
-+ dse_ldif.delete(parentid_dn, "nsMatchingRule", mr)
-+
-+ log.info("Run index-check without --fix (should detect all issues)")
-+ args = FakeArgs()
-+ args.backend = "userRoot"
-+ args.fix = False
-+
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is False, "index-check should detect multiple issues"
-+ # Check that multiple issues were detected
-+ assert topology_st.logcap.contains("nsIndexIDListScanLimit")
-+ assert topology_st.logcap.contains("ancestorid")
-+ topology_st.logcap.flush()
-+
-+ log.info("Run index-check with --fix")
-+ args.fix = True
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is True, "index-check --fix should succeed"
-+ assert topology_st.logcap.contains("All issues fixed")
-+ topology_st.logcap.flush()
-+
-+ log.info("Verify all issues were fixed")
-+ dse_ldif = DSEldif(standalone) # Reload
-+
-+ # Check scanlimit removed
-+ scanlimit = dse_ldif.get(parentid_dn, "nsIndexIDListScanLimit", single=True)
-+ assert scanlimit is None, f"nsIndexIDListScanLimit should be removed, got: {scanlimit}"
-+
-+ # Check ancestorid config removed
-+ cn_value = dse_ldif.get(ancestorid_dn, "cn", single=True)
-+ assert cn_value is None, f"ancestorid config should be removed, got: {cn_value}"
-+
-+ # Check matching rule added back
-+ matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
-+ found_int_order = False
-+ if matching_rules:
-+ for mr in matching_rules:
-+ if "integerorderingmatch" in mr.lower():
-+ found_int_order = True
-+ break
-+ assert found_int_order, f"integerOrderingMatch should be present, got: {matching_rules}"
-+
-+ log.info("All issues verified as fixed")
-+
-+ log.info("Run index-check again to confirm all clear")
-+ args.fix = False
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is True, "index-check should pass after fix"
-+ assert topology_st.logcap.contains("All checks passed")
-+ topology_st.logcap.flush()
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
- if __name__ == "__main__":
- # Run isolated
- # -s for DEBUG mode
-diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in
-index 51bfd7e77..ae359ab6b 100644
---- a/rpm/389-ds-base.spec.in
-+++ b/rpm/389-ds-base.spec.in
-@@ -628,42 +628,45 @@ if ! getent passwd $USERNAME >/dev/null ; then
- fi
-
- # Reload our sysctl before we restart (if we can)
--sysctl --system &> $output; true
-+sysctl --system &> "$output"; true
-
--# Gather the running instances so we can restart them
-+# Gather running instances, stop them, run index-check, then restart
- instbase="%{_sysconfdir}/%{pkgname}"
-+instances=""
- ninst=0
--for dir in $instbase/slapd-* ; do
-- echo dir = $dir >> $output 2>&1 || :
-+
-+for dir in "$instbase"/slapd-* ; do
-+ echo "dir = $dir" >> "$output" 2>&1 || :
- if [ ! -d "$dir" ] ; then continue ; fi
- case "$dir" in *.removed) continue ;; esac
-- basename=`basename $dir`
-- inst="%{pkgname}@`echo $basename | sed -e 's/slapd-//g'`"
-- echo found instance $inst - getting status >> $output 2>&1 || :
-- if /bin/systemctl -q is-active $inst ; then
-- echo instance $inst is running >> $output 2>&1 || :
-+ basename=$(basename "$dir")
-+ inst="%{pkgname}@${basename#slapd-}"
-+ inst_name="${basename#slapd-}"
-+ echo "found instance $inst - getting status" >> "$output" 2>&1 || :
-+ if /bin/systemctl -q is-active "$inst" ; then
-+ echo "instance $inst is running - stopping for upgrade" >> "$output" 2>&1 || :
- instances="$instances $inst"
-+ /bin/systemctl stop "$inst" >> "$output" 2>&1 || :
- else
-- echo instance $inst is not running >> $output 2>&1 || :
-+ echo "instance $inst is not running" >> "$output" 2>&1 || :
- fi
-- ninst=`expr $ninst + 1`
-+ # Run index-check on all instances (running or not)
-+ # This fixes index ordering mismatches from older versions
-+ dsctl "$inst_name" index-check --fix >> "$output2" 2>&1 || :
-+ ninst=$((ninst + 1))
- done
-+
- if [ $ninst -eq 0 ] ; then
-- echo no instances to upgrade >> $output 2>&1 || :
-- exit 0 # have no instances to upgrade - just skip the rest
--else
-- # restart running instances
-- echo shutting down all instances . . . >> $output 2>&1 || :
-- for inst in $instances ; do
-- echo stopping instance $inst >> $output 2>&1 || :
-- /bin/systemctl stop $inst >> $output 2>&1 || :
-- done
-- for inst in $instances ; do
-- echo starting instance $inst >> $output 2>&1 || :
-- /bin/systemctl start $inst >> $output 2>&1 || :
-- done
-+ echo "no instances to upgrade" >> "$output" 2>&1 || :
-+ exit 0
- fi
-
-+# Restart previously running instances
-+for inst in $instances ; do
-+ echo "starting instance $inst" >> "$output" 2>&1 || :
-+ /bin/systemctl start "$inst" >> "$output" 2>&1 || :
-+done
-+
-
- %preun
- if [ $1 -eq 0 ]; then # Final removal
-diff --git a/src/lib389/lib389/cli_ctl/dbtasks.py b/src/lib389/lib389/cli_ctl/dbtasks.py
-index 856639672..16da966d1 100644
---- a/src/lib389/lib389/cli_ctl/dbtasks.py
-+++ b/src/lib389/lib389/cli_ctl/dbtasks.py
-@@ -7,12 +7,24 @@
- # See LICENSE for details.
- # --- END COPYRIGHT BLOCK ---
-
-+import glob
- import os
-+import re
-+import subprocess
-+from enum import Enum
- from lib389._constants import TaskWarning
- from lib389.cli_base import CustomHelpFormatter
-+from lib389.dseldif import DSEldif
- from pathlib import Path
-
-
-+class IndexOrdering(Enum):
-+ """Represents the ordering type of an index."""
-+ INTEGER = "integer"
-+ LEXICOGRAPHIC = "lexicographic"
-+ UNKNOWN = "unknown"
-+
-+
- def dbtasks_db2index(inst, log, args):
- rtn = False
- if not args.backend:
-@@ -126,6 +138,387 @@ def dbtasks_verify(inst, log, args):
- log.info("dbverify successful")
-
-
-+def _get_db_dir(dse_ldif):
-+ """Get the database directory.
-+
-+ Args:
-+ dse_ldif: DSEldif instance.
-+
-+ Returns:
-+ Path to the database directory, or None if not found.
-+ """
-+ try:
-+ db_dir = dse_ldif.get(
-+ "cn=config,cn=ldbm database,cn=plugins,cn=config",
-+ "nsslapd-directory",
-+ single=True,
-+ )
-+ return db_dir
-+ except (ValueError, TypeError):
-+ pass
-+ return None
-+
-+
-+
-+def _has_integer_ordering_match(dse_ldif, backend, index_name):
-+ """Check if an index has integerOrderingMatch configured in DSE.
-+
-+ Args:
-+ dse_ldif: DSEldif instance.
-+ backend: Backend name.
-+ index_name: Name of the index to check.
-+
-+ Returns:
-+ True if integerOrderingMatch is configured, False otherwise.
-+ """
-+ index_dn = "cn={},cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(
-+ index_name, backend
-+ )
-+ matching_rules = dse_ldif.get(index_dn, "nsMatchingRule", lower=True)
-+ if matching_rules:
-+ return any(mr.lower() == "integerorderingmatch" for mr in matching_rules)
-+ return False
-+
-+
-+def _has_index_scan_limit(dse_ldif, backend, index_name):
-+ """Check if an index has nsIndexIDListScanLimit configured.
-+
-+ Args:
-+ dse_ldif: DSEldif instance.
-+ backend: Backend name.
-+ index_name: Name of the index to check.
-+
-+ Returns:
-+ True if nsIndexIDListScanLimit is configured, False otherwise.
-+ """
-+ index_dn = "cn={},cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(
-+ index_name, backend
-+ )
-+ scan_limit = dse_ldif.get(index_dn, "nsIndexIDListScanLimit")
-+ return scan_limit is not None
-+
-+
-+def _index_config_exists(dse_ldif, backend, index_name):
-+ """Check if an index configuration entry exists in DSE.
-+
-+ Args:
-+ dse_ldif: DSEldif instance.
-+ backend: Backend name.
-+ index_name: Name of the index to check.
-+
-+ Returns:
-+ True if the index config entry exists, False otherwise.
-+ """
-+ index_dn = "cn={},cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(
-+ index_name, backend
-+ )
-+ try:
-+ cn = dse_ldif.get(index_dn, "cn")
-+ return cn is not None
-+ except (ValueError, KeyError):
-+ return False
-+
-+
-+def _default_index_exists(dse_ldif, index_name):
-+ """Check if an index exists in cn=default indexes.
-+
-+ Args:
-+ dse_ldif: DSEldif instance.
-+ index_name: Name of the index to check.
-+
-+ Returns:
-+ True if the index exists in default indexes, False otherwise.
-+ """
-+ index_dn = "cn={},cn=default indexes,cn=config,cn=ldbm database,cn=plugins,cn=config".format(
-+ index_name
-+ )
-+ try:
-+ cn = dse_ldif.get(index_dn, "cn")
-+ return cn is not None
-+ except (ValueError, KeyError):
-+ return False
-+
-+
-+def _check_disk_ordering(db_dir, backend, index_name, dbscan_path, is_mdb, log):
-+ """Check if index on disk uses lexicographic or integer ordering.
-+
-+ Args:
-+ db_dir: Path to the database directory.
-+ backend: Backend name.
-+ index_name: Name of the index to check.
-+ dbscan_path: Path to the dbscan binary.
-+ is_mdb: True if using MDB backend.
-+ log: Logger instance.
-+
-+ Returns:
-+ IndexOrdering: The detected ordering type.
-+ """
-+ if is_mdb:
-+ # MDB uses pseudo-paths: db_dir/backend/index.db
-+ # dbscan accesses indexes via paths like: /var/lib/dirsrv/slapd-xxx/db/userroot/parentid.db
-+ index_file = os.path.join(db_dir, backend, "{}.db".format(index_name))
-+ else:
-+ # BDB has separate directories per backend with actual index files
-+ backend_dir = os.path.join(db_dir, backend)
-+ if not os.path.exists(backend_dir):
-+ return IndexOrdering.UNKNOWN
-+ index_file = None
-+ pattern = os.path.join(backend_dir, "{}.db*".format(index_name))
-+ for f in glob.glob(pattern):
-+ if os.path.isfile(f):
-+ index_file = f
-+ break
-+ if not index_file:
-+ return IndexOrdering.UNKNOWN
-+
-+ try:
-+ result = subprocess.run(
-+ [dbscan_path, "-f", index_file],
-+ stdout=subprocess.PIPE,
-+ stderr=subprocess.PIPE,
-+ universal_newlines=True,
-+ timeout=60,
-+ )
-+
-+ if result.returncode != 0:
-+ log.warning(" dbscan returned non-zero exit code for %s", index_file)
-+ return IndexOrdering.UNKNOWN
-+
-+ # Parse keys from dbscan output
-+ keys = []
-+ for line in result.stdout.split("\n"):
-+ line = line.strip()
-+ if line.startswith("="):
-+ match = re.match(r"^=(\d+)", line)
-+ if match:
-+ keys.append(int(match.group(1)))
-+
-+ if len(keys) < 2:
-+ return IndexOrdering.UNKNOWN
-+
-+ # Check if keys are in integer order by looking for decreasing numeric values
-+ # (which would indicate lexicographic ordering, e.g., "3" < "30" < "4")
-+ prev_id = keys[0]
-+ for i in range(1, min(len(keys), 100)):
-+ current_id = keys[i]
-+ if prev_id > current_id:
-+ return IndexOrdering.LEXICOGRAPHIC
-+ prev_id = current_id
-+
-+ return IndexOrdering.INTEGER
-+
-+ except subprocess.TimeoutExpired:
-+ log.warning(" dbscan timed out for %s", index_file)
-+ return IndexOrdering.UNKNOWN
-+ except OSError as e:
-+ log.warning(" Error running dbscan: %s", e)
-+ return IndexOrdering.UNKNOWN
-+
-+
-+def dbtasks_index_check(inst, log, args):
-+ """Check and optionally fix index ordering mismatches.
-+
-+ This function detects mismatches between the configured ordering
-+ (integerOrderingMatch in DSE) and the actual on-disk ordering of
-+ parentid and ancestorid indexes.
-+
-+ Args:
-+ inst: DirSrv instance.
-+ log: Logger instance.
-+ args: Parsed command line arguments.
-+
-+ Returns:
-+ True if all checks passed, False if mismatches were detected.
-+ """
-+ # Server must be stopped
-+ if inst.status():
-+ log.error("index-check requires the instance to be stopped")
-+ return False
-+
-+ # Check for dbscan binary
-+ dbscan_path = os.path.join(inst.ds_paths.bin_dir, "dbscan")
-+ if not os.path.exists(dbscan_path):
-+ log.error("dbscan utility not found at %s", dbscan_path)
-+ return False
-+
-+ # Load DSE
-+ try:
-+ dse_ldif = DSEldif(inst)
-+ except Exception as e:
-+ log.error("Failed to read dse.ldif: %s", e)
-+ return False
-+
-+ # Get backends to check
-+ all_backends = dse_ldif.get_backends()
-+ if not all_backends:
-+ log.info("No backends found")
-+ return True
-+
-+ # Filter to specific backend if requested
-+ if args.backend:
-+ # Case-insensitive backend lookup
-+ backend_lower = args.backend.lower()
-+ matching_backend = None
-+ for be in all_backends:
-+ if be.lower() == backend_lower:
-+ matching_backend = be
-+ break
-+ if matching_backend is None:
-+ log.error("Backend '%s' not found. Available backends: %s",
-+ args.backend, ", ".join(all_backends))
-+ return False
-+ backends_to_check = [matching_backend]
-+ else:
-+ backends_to_check = all_backends
-+
-+ # Get database directory and check database type
-+ db_dir = _get_db_dir(dse_ldif)
-+ if not db_dir or not os.path.exists(db_dir):
-+ log.error("Database directory not found")
-+ return False
-+
-+ db_lib = inst.get_db_lib()
-+ is_mdb = (db_lib == "mdb")
-+ log.info("Database type: %s", db_lib.upper())
-+
-+ # Track all issues found
-+ all_ok = True
-+ mismatches = [] # (backend, index_name) tuples needing reindex
-+ missing_matching_rules = [] # (backend, index_name) tuples missing integerOrderingMatch
-+ scan_limits_to_remove = [] # (backend, index_name) tuples with nsIndexIDListScanLimit
-+ ancestorid_configs_to_remove = [] # backend names with ancestorid config entries
-+ remove_ancestorid_from_defaults = False # Flag to remove from cn=default indexes
-+
-+ # Check if ancestorid exists in cn=default indexes (should be removed)
-+ if _default_index_exists(dse_ldif, "ancestorid"):
-+ log.warning("ancestorid found in cn=default indexes - should be removed")
-+ remove_ancestorid_from_defaults = True
-+ all_ok = False
-+
-+ for backend in backends_to_check:
-+ log.info("Checking backend: %s", backend)
-+
-+ # Check for ancestorid config entry (should not exist)
-+ if _index_config_exists(dse_ldif, backend, "ancestorid"):
-+ log.warning(" ancestorid - config entry exists (should be removed)")
-+ ancestorid_configs_to_remove.append(backend)
-+ all_ok = False
-+
-+ # Check parentid and ancestorid indexes
-+ for index_name in ["parentid", "ancestorid"]:
-+ # Check for scan limits (should be removed)
-+ if _has_index_scan_limit(dse_ldif, backend, index_name):
-+ log.warning(" %s - has nsIndexIDListScanLimit (should be removed)", index_name)
-+ scan_limits_to_remove.append((backend, index_name))
-+ all_ok = False
-+
-+ # Check disk ordering
-+ disk_ordering = _check_disk_ordering(db_dir, backend, index_name, dbscan_path, is_mdb, log)
-+
-+ if disk_ordering == IndexOrdering.UNKNOWN:
-+ log.info(" %s - could not determine disk ordering, skipping", index_name)
-+ # For parentid, still check if matching rule is missing
-+ if index_name == "parentid":
-+ config_has_int_order = _has_integer_ordering_match(dse_ldif, backend, index_name)
-+ if not config_has_int_order:
-+ log.warning(" %s - missing integerOrderingMatch in config", index_name)
-+ missing_matching_rules.append((backend, index_name))
-+ all_ok = False
-+ continue
-+
-+ config_has_int_order = _has_integer_ordering_match(dse_ldif, backend, index_name)
-+ config_desc = "integer" if config_has_int_order else "lexicographic"
-+ log.info(" %s - config: %s, disk: %s",
-+ index_name, config_desc, disk_ordering.value)
-+
-+ # For parentid, the desired state is always integer ordering
-+ if index_name == "parentid":
-+ if not config_has_int_order:
-+ log.warning(" %s - missing integerOrderingMatch in config", index_name)
-+ if (backend, index_name) not in missing_matching_rules:
-+ missing_matching_rules.append((backend, index_name))
-+ all_ok = False
-+
-+ if disk_ordering == IndexOrdering.LEXICOGRAPHIC:
-+ log.warning(" %s - disk ordering is lexicographic, needs reindex", index_name)
-+ if (backend, index_name) not in mismatches:
-+ mismatches.append((backend, index_name))
-+ all_ok = False
-+
-+ # Handle issues
-+ if not all_ok:
-+ if args.fix:
-+ log.info("Fixing issues...")
-+
-+ # Remove ancestorid from cn=default indexes
-+ if remove_ancestorid_from_defaults:
-+ default_idx_dn = "cn=ancestorid,cn=default indexes,cn=config,cn=ldbm database,cn=plugins,cn=config"
-+ log.info(" Removing ancestorid from default indexes...")
-+ try:
-+ dse_ldif.delete_dn(default_idx_dn)
-+ log.info(" Removed ancestorid from default indexes")
-+ except Exception as e:
-+ log.error(" Failed to remove ancestorid from default indexes: %s", e)
-+ return False
-+
-+ # Remove scan limits (only for indexes that won't be deleted)
-+ for backend, index_name in scan_limits_to_remove:
-+ # Skip ancestorid if we're going to delete the whole entry anyway
-+ if index_name == "ancestorid" and backend in ancestorid_configs_to_remove:
-+ continue
-+ index_dn = "cn={},cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(
-+ index_name, backend
-+ )
-+ log.info(" Removing nsIndexIDListScanLimit from %s in backend %s...", index_name, backend)
-+ try:
-+ dse_ldif.delete(index_dn, "nsIndexIDListScanLimit")
-+ log.info(" Removed nsIndexIDListScanLimit from %s", index_name)
-+ except Exception as e:
-+ log.error(" Failed to remove nsIndexIDListScanLimit from %s: %s", index_name, e)
-+ return False
-+
-+ # Remove ancestorid config entries from backends
-+ for backend in ancestorid_configs_to_remove:
-+ index_dn = "cn=ancestorid,cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(backend)
-+ log.info(" Removing ancestorid config entry from backend %s...", backend)
-+ try:
-+ dse_ldif.delete_dn(index_dn)
-+ log.info(" Removed ancestorid config entry from backend %s", backend)
-+ except Exception as e:
-+ log.error(" Failed to remove ancestorid config from backend %s: %s", backend, e)
-+ return False
-+
-+ # Add missing matching rules to dse.ldif
-+ for backend, index_name in missing_matching_rules:
-+ index_dn = "cn={},cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(
-+ index_name, backend
-+ )
-+ log.info(" Adding integerOrderingMatch to %s in backend %s...", index_name, backend)
-+ try:
-+ dse_ldif.add(index_dn, "nsMatchingRule", "integerOrderingMatch")
-+ log.info(" Updated dse.ldif with integerOrderingMatch for %s", index_name)
-+ except Exception as e:
-+ log.error(" Failed to update dse.ldif for %s: %s", index_name, e)
-+ return False
-+
-+ # Reindex indexes with disk ordering issues
-+ for backend, index_name in mismatches:
-+ log.info(" Reindexing %s in backend %s...", index_name, backend)
-+ if not inst.db2index(bename=backend, attrs=[index_name]):
-+ log.error(" Failed to reindex %s", index_name)
-+ return False
-+ log.info(" Reindex of %s completed successfully", index_name)
-+
-+ log.info("All issues fixed")
-+ return True
-+ else:
-+ log.info("Issues detected. Run with --fix to repair.")
-+ return False
-+ else:
-+ log.info("All checks passed - no issues found")
-+ return True
-+
-+
- def create_parser(subcommands):
- db2index_parser = subcommands.add_parser('db2index', help="Initialise a reindex of the server database. The server must be stopped for this to proceed.", formatter_class=CustomHelpFormatter)
- # db2index_parser.add_argument('suffix', help="The suffix to reindex. IE dc=example,dc=com.")
-@@ -172,3 +565,12 @@ def create_parser(subcommands):
- ldifs_parser = subcommands.add_parser('ldifs', help="List all the LDIF files located in the server's LDIF directory", formatter_class=CustomHelpFormatter)
- ldifs_parser.add_argument('--delete', nargs=1, help="Delete LDIF file")
- ldifs_parser.set_defaults(func=dbtasks_ldifs)
-+
-+ index_check_parser = subcommands.add_parser('index-check',
-+ help="Check for index ordering mismatches (parentid/ancestorid). The server must be stopped.",
-+ formatter_class=CustomHelpFormatter)
-+ index_check_parser.add_argument('backend', nargs='?', default=None,
-+ help="Backend to check. If not specified, all backends are checked.")
-+ index_check_parser.add_argument('--fix', action='store_true', default=False,
-+ help="Fix mismatches by reindexing affected indexes")
-+ index_check_parser.set_defaults(func=dbtasks_index_check)
-diff --git a/src/lib389/lib389/dseldif.py b/src/lib389/lib389/dseldif.py
-index d12c6424c..7834d9468 100644
---- a/src/lib389/lib389/dseldif.py
-+++ b/src/lib389/lib389/dseldif.py
-@@ -125,11 +125,14 @@ class DSEldif(DSLint):
- self._contents[i] = self._contents[i].replace(strfrom, strto)
- self._update()
-
-- def _find_attr(self, entry_dn, attr):
-+ def _find_attr(self, entry_dn, attr, lower=False):
- """Find all attribute values and indexes under a given entry
-
- Returns entry dn index and attribute data dict:
- relative attribute indexes and the attribute value
-+
-+ :param lower: Use case-insensitive matching for attribute name
-+ :type lower: boolean
- """
-
- entry_dn_i = self._contents.index("dn: {}\n".format(entry_dn.lower()))
-@@ -146,7 +149,11 @@ class DSEldif(DSLint):
-
- # Find the attribute
- for line in entry_slice:
-- if line.startswith("{}:".format(attr)):
-+ if lower:
-+ match = line.lower().startswith("{}:".format(attr.lower()))
-+ else:
-+ match = line.startswith("{}:".format(attr))
-+ if match:
- attr_value = line.split(" ", 1)[1][:-1]
- attr_data.update({entry_slice.index(line): attr_value})
-
-@@ -155,7 +162,7 @@ class DSEldif(DSLint):
-
- return entry_dn_i, attr_data
-
-- def get(self, entry_dn, attr, single=False):
-+ def get(self, entry_dn, attr, single=False, lower=False):
- """Return attribute values under a given entry
-
- :param entry_dn: a DN of entry we want to get attribute from
-@@ -163,11 +170,13 @@ class DSEldif(DSLint):
- :param attr: an attribute name
- :type attr: str
- :param single: Return a single value instead of a list
-- :type sigle: boolean
-+ :type single: boolean
-+ :param lower: Use case-insensitive matching for attribute name
-+ :type lower: boolean
- """
-
- try:
-- _, attr_data = self._find_attr(entry_dn, attr)
-+ _, attr_data = self._find_attr(entry_dn, attr, lower=lower)
- except ValueError:
- return None
-
-@@ -190,6 +199,38 @@ class DSEldif(DSLint):
-
- return indexes
-
-+ def get_backends(self):
-+ """Return a list of backend names from DSE.
-+
-+ Returns backend names preserving their original case, as the
-+ database directory names on disk use the original case.
-+
-+ Note: DSEldif lowercases DN lines, so we read the 'cn' attribute
-+ from each entry to get the original case.
-+
-+ :returns: List of backend names
-+ """
-+ backends = []
-+ excluded = ("config", "monitor", "index", "encrypted attributes")
-+
-+ for entry in self._contents:
-+ if (entry.startswith("dn: cn=") and
-+ ",cn=ldbm database,cn=plugins,cn=config" in entry):
-+ parts = entry.split(",")
-+ if len(parts) > 1:
-+ cn_lower = parts[0].replace("dn: cn=", "")
-+ if cn_lower not in excluded:
-+ dn = entry.strip()[4:].strip()
-+ try:
-+ suffix = self.get(dn, "nsslapd-suffix")
-+ if suffix:
-+ cn_values = self.get(dn, "cn")
-+ if cn_values:
-+ backends.append(cn_values[0])
-+ except (ValueError, IndexError):
-+ pass
-+
-+ return list(set(backends))
-
- def add_entry(self, entry):
- """Add a new entry
---
-2.52.0
-
diff --git a/0023-Issue-7184-2nd-argparse.HelpFormatter-_format_action.patch b/0023-Issue-7184-2nd-argparse.HelpFormatter-_format_action.patch
deleted file mode 100644
index 616bf44..0000000
--- a/0023-Issue-7184-2nd-argparse.HelpFormatter-_format_action.patch
+++ /dev/null
@@ -1,41 +0,0 @@
-From fcec1f00b99b227d44a2f0aef6e3a87ddb7e52a9 Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Fri, 13 Feb 2026 15:38:52 +0100
-Subject: [PATCH] Issue 7184 - (2nd) argparse.HelpFormatter
- _format_actions_usage() is deprecated (#7257)
-
-Description:
-`_format_actions_usage()` was also removed in Python 3.14.3.
-Replace version check with `isinstance()` to handle the return type of
-`_get_actions_usage_parts()` more robustly across Python versions.
-
-Relates: https://github.com/389ds/389-ds-base/issues/7184
-Fixes: https://github.com/389ds/389-ds-base/issues/7253
-
-Reviewed by: @progier389 (Thanks!)
----
- src/lib389/lib389/cli_base/__init__.py | 6 +++---
- 1 file changed, 3 insertions(+), 3 deletions(-)
-
-diff --git a/src/lib389/lib389/cli_base/__init__.py b/src/lib389/lib389/cli_base/__init__.py
-index f1055aadc..3af8a46e6 100644
---- a/src/lib389/lib389/cli_base/__init__.py
-+++ b/src/lib389/lib389/cli_base/__init__.py
-@@ -420,11 +420,11 @@ class CustomHelpFormatter(argparse.HelpFormatter):
- else:
- # Use _get_actions_usage_parts() for Python 3.13 and later
- action_parts = self._get_actions_usage_parts(parent_arguments, [])
-- if sys.version_info >= (3, 15):
-- # Python 3.15 returns a tuple (list of actions, count of actions)
-+ if isinstance(action_parts, tuple):
-+ # Python 3.14.3+ and 3.15+ return a tuple (list of actions, count of actions)
- formatted_options = ' '.join(action_parts[0])
- else:
-- # Python 3.13 and 3.14 return a list of actions
-+ # Earlier versions return a list of actions
- formatted_options = ' '.join(action_parts)
-
- # If formatted_options already in usage - remove them
---
-2.52.0
-
diff --git a/0024-Issue-7223-Use-lexicographical-order-for-ancestorid-.patch b/0024-Issue-7223-Use-lexicographical-order-for-ancestorid-.patch
deleted file mode 100644
index c0c9844..0000000
--- a/0024-Issue-7223-Use-lexicographical-order-for-ancestorid-.patch
+++ /dev/null
@@ -1,35 +0,0 @@
-From 7e575cc8cc6f1bf558f50ca0fc55145e469d60d2 Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Fri, 13 Feb 2026 16:58:24 +0100
-Subject: [PATCH 1/2] Issue 7223 - Use lexicographical order for ancestorid
- (#7256)
-
-Description:
-`ldbm_instance_create_default_indexes()` configured ancestorid with
-integerOrderingMatch in the in-memory attrinfo, but ancestorid on disk
-might be using lexicographic ordering (data before the upgrade or after
-ldif2db import).
-
-Relates: https://github.com/389ds/389-ds-base/issues/7223
-
-Reviewed by: @tbordaz (Thanks!)
----
- ldap/servers/slapd/back-ldbm/instance.c | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/ldap/servers/slapd/back-ldbm/instance.c b/ldap/servers/slapd/back-ldbm/instance.c
-index 17bfc09a0..1569eb7ff 100644
---- a/ldap/servers/slapd/back-ldbm/instance.c
-+++ b/ldap/servers/slapd/back-ldbm/instance.c
-@@ -231,7 +231,7 @@ ldbm_instance_create_default_indexes(backend *be)
- * ancestorid is special, there is actually no such attr type
- * but we still want to use the attr index file APIs.
- */
-- e = ldbm_instance_init_config_entry(LDBM_ANCESTORID_STR, "eq", 0, 0, 0, "integerOrderingMatch");
-+ e = ldbm_instance_init_config_entry(LDBM_ANCESTORID_STR, "eq", 0, 0, 0, 0);
- attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
- slapi_entry_free(e);
-
---
-2.52.0
-
diff --git a/0025-Issue-7223-Remove-integerOrderingMatch-requirement-f.patch b/0025-Issue-7223-Remove-integerOrderingMatch-requirement-f.patch
deleted file mode 100644
index a5c1292..0000000
--- a/0025-Issue-7223-Remove-integerOrderingMatch-requirement-f.patch
+++ /dev/null
@@ -1,538 +0,0 @@
-From 273e0a92d902a2f2989f8def0f4fb42b7c08cae9 Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Wed, 18 Feb 2026 09:26:57 +0100
-Subject: [PATCH] Issue 7223 - Remove integerOrderingMatch requirement for
- parentid (#7264)
-
-Description:
-integerOrderingMatch was introduced as a requirement for parentid and
-ancestorid indexes for performance reasons. But after #7096 the order
-for parentid doesn't make a lot of difference.
-
-Fix Description:
-* Remove integerOrderingMatch requirement for parentid.
-* Read only first 100 keys from dbscan in index ordering check
-* Do not run dsctl index-check during RPM upgrade
-
-Relates: https://github.com/389ds/389-ds-base/pull/7223
-
-Reviewed by: @progier389, @tbordaz (Thanks!)
----
- .../healthcheck/health_system_indexes_test.py | 83 ++++----------
- ldap/servers/slapd/upgrade.c | 105 ------------------
- rpm/389-ds-base.spec.in | 3 -
- src/lib389/lib389/backend.py | 5 +-
- src/lib389/lib389/cli_ctl/dbtasks.py | 99 ++++++++---------
- 5 files changed, 73 insertions(+), 222 deletions(-)
-
-diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-index 6fbcf666c..9c48bd79b 100644
---- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-@@ -180,7 +180,8 @@ def test_missing_parentid(topology_st, log_buffering_enabled):
-
-
- def test_missing_matching_rule(topology_st, log_buffering_enabled):
-- """Check if healthcheck returns DSBLE0007 code when parentId index is missing integerOrderingMatch
-+ """Check that healthcheck does NOT report DSBLE0007 when parentId index is missing integerOrderingMatch.
-+ Both lexicographic and integer orderings are valid for parentid.
-
- :id: 7ffa71db-8995-430a-bed8-59bce944221c
- :setup: Standalone instance
-@@ -190,19 +191,14 @@ def test_missing_matching_rule(topology_st, log_buffering_enabled):
- 3. Use healthcheck without --json option
- 4. Use healthcheck with --json option
- 5. Re-add the matching rule
-- 6. Use healthcheck without --json option
-- 7. Use healthcheck with --json option
- :expectedresults:
- 1. Success
- 2. Success
-- 3. healthcheck reports DSBLE0007 code and related details
-- 4. healthcheck reports DSBLE0007 code and related details
-+ 3. healthcheck reports no issues found
-+ 4. healthcheck reports no issues found
- 5. Success
-- 6. healthcheck reports no issues found
-- 7. healthcheck reports no issues found
- """
-
-- RET_CODE = "DSBLE0007"
- PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
-
- standalone = topology_st.standalone
-@@ -211,17 +207,14 @@ def test_missing_matching_rule(topology_st, log_buffering_enabled):
- parentid_index = Index(standalone, PARENTID_DN)
- parentid_index.remove("nsMatchingRule", "integerOrderingMatch")
-
-- run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=RET_CODE)
-- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=RET_CODE)
-+ run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
-+ run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
-
- log.info("Re-add the integerOrderingMatch matching rule")
- parentid_index = Index(standalone, PARENTID_DN)
- parentid_index.add("nsMatchingRule", "integerOrderingMatch")
- standalone.restart()
-
-- run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
-- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
--
-
- def test_usn_plugin_missing_entryusn(topology_st, usn_plugin_enabled, log_buffering_enabled):
- """Check if healthcheck returns DSBLE0007 code when USN plugin is enabled but entryusn index is missing
-@@ -911,7 +904,9 @@ def test_index_check_fixes_ancestorid_config(topology_st):
-
-
- def test_index_check_fixes_missing_matching_rule(topology_st):
-- """Check if dsctl index-check --fix adds missing integerOrderingMatch
-+ """Check that removing integerOrderingMatch from parentid config is not
-+ flagged as an issue when disk ordering cannot be determined.
-+ Both lexicographic and integer orderings are valid for parentid.
-
- :id: 6c1d4e9f-0a3b-4d5c-1e7f-8a9b0c2d3e4f
- :setup: Standalone instance
-@@ -919,18 +914,14 @@ def test_index_check_fixes_missing_matching_rule(topology_st):
- 1. Create DS instance
- 2. Stop the server
- 3. Remove integerOrderingMatch from parentid index using DSEldif
-- 4. Run dsctl index-check (should detect issue)
-- 5. Run dsctl index-check --fix
-- 6. Verify integerOrderingMatch was added back
-- 7. Start the server
-+ 4. Run dsctl index-check (should NOT detect issue since disk ordering is unknown)
-+ 5. Start the server
- :expectedresults:
- 1. Success
- 2. Success
- 3. Success
-- 4. index-check returns False and detects missing matching rule
-- 5. index-check returns True after fix
-- 6. integerOrderingMatch is present
-- 7. Success
-+ 4. index-check returns True (no issues, disk ordering unknown)
-+ 5. Success
- """
- from lib389.cli_ctl.dbtasks import dbtasks_index_check
- from lib389.dseldif import DSEldif
-@@ -964,34 +955,20 @@ def test_index_check_fixes_missing_matching_rule(topology_st):
- f"integerOrderingMatch should be removed, but found: {mr}"
- log.info("integerOrderingMatch removed from parentid index")
-
-- log.info("Run index-check without --fix (should detect issue)")
-+ log.info("Run index-check (should NOT detect issue - disk ordering unknown)")
- args = FakeArgs()
- args.backend = "userRoot"
- args.fix = False
-
- result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-- assert result is False, "index-check should detect missing matching rule"
-- assert topology_st.logcap.contains("missing integerOrderingMatch")
-+ assert result is True, \
-+ "index-check should not flag missing integerOrderingMatch when disk ordering is unknown"
-+ assert topology_st.logcap.contains("could not determine disk ordering")
- topology_st.logcap.flush()
-
-- log.info("Run index-check with --fix")
-- args.fix = True
-- result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-- assert result is True, "index-check --fix should succeed"
-- assert topology_st.logcap.contains("integerOrderingMatch")
-- topology_st.logcap.flush()
--
-- log.info("Verify integerOrderingMatch was added back")
-- dse_ldif = DSEldif(standalone) # Reload to get fresh data
-- matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
-- assert matching_rules is not None, "nsMatchingRule should be present"
-- found_int_order = False
-- for mr in matching_rules:
-- if "integerorderingmatch" in mr.lower():
-- found_int_order = True
-- break
-- assert found_int_order, f"integerOrderingMatch should be present, got: {matching_rules}"
-- log.info("integerOrderingMatch successfully added back")
-+ log.info("Restore integerOrderingMatch and start the server")
-+ dse_ldif = DSEldif(standalone)
-+ dse_ldif.add(parentid_dn, "nsMatchingRule", "integerOrderingMatch")
-
- log.info("Start the server")
- standalone.start()
-@@ -1081,7 +1058,7 @@ def test_index_check_fixes_multiple_issues(topology_st):
- :steps:
- 1. Create DS instance
- 2. Stop the server
-- 3. Add multiple issues: scanlimit, ancestorid config, missing matching rule
-+ 3. Add multiple issues: scanlimit and ancestorid config
- 4. Run dsctl index-check (should detect all issues)
- 5. Run dsctl index-check --fix
- 6. Verify all issues were fixed
-@@ -1123,14 +1100,6 @@ def test_index_check_fixes_multiple_issues(topology_st):
- ]
- dse_ldif.add_entry(ancestorid_entry)
-
-- log.info("Add issue 3: Remove integerOrderingMatch from parentid")
-- dse_ldif = DSEldif(standalone) # Reload
-- matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
-- if matching_rules:
-- for mr in matching_rules:
-- if "integerorderingmatch" in mr.lower():
-- dse_ldif.delete(parentid_dn, "nsMatchingRule", mr)
--
- log.info("Run index-check without --fix (should detect all issues)")
- args = FakeArgs()
- args.backend = "userRoot"
-@@ -1161,16 +1130,6 @@ def test_index_check_fixes_multiple_issues(topology_st):
- cn_value = dse_ldif.get(ancestorid_dn, "cn", single=True)
- assert cn_value is None, f"ancestorid config should be removed, got: {cn_value}"
-
-- # Check matching rule added back
-- matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
-- found_int_order = False
-- if matching_rules:
-- for mr in matching_rules:
-- if "integerorderingmatch" in mr.lower():
-- found_int_order = True
-- break
-- assert found_int_order, f"integerOrderingMatch should be present, got: {matching_rules}"
--
- log.info("All issues verified as fixed")
-
- log.info("Run index-check again to confirm all clear")
-diff --git a/ldap/servers/slapd/upgrade.c b/ldap/servers/slapd/upgrade.c
-index 6b1b012da..9557e9066 100644
---- a/ldap/servers/slapd/upgrade.c
-+++ b/ldap/servers/slapd/upgrade.c
-@@ -551,107 +551,6 @@ upgrade_remove_ancestorid_index_config(void)
- return uresult;
- }
-
--/*
-- * Check if parentid/ancestorid indexes are missing the integerOrderingMatch
-- * matching rule.
-- *
-- * This function logs a warning if we detect this condition, advising
-- * the administrator to reindex the affected attributes.
-- */
--static upgrade_status
--upgrade_check_id_index_matching_rule(void)
--{
-- struct slapi_pblock *pb = slapi_pblock_new();
-- Slapi_Entry **backends = NULL;
-- const char *be_base_dn = "cn=ldbm database,cn=plugins,cn=config";
-- const char *be_filter = "(objectclass=nsBackendInstance)";
-- const char *attrs_to_check[] = {"parentid", NULL};
-- upgrade_status uresult = UPGRADE_SUCCESS;
--
-- /* Search for all backend instances */
-- slapi_search_internal_set_pb(
-- pb, be_base_dn,
-- LDAP_SCOPE_ONELEVEL,
-- be_filter, NULL, 0, NULL, NULL,
-- plugin_get_default_component_id(), 0);
-- slapi_search_internal_pb(pb);
-- slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &backends);
--
-- if (backends) {
-- for (size_t be_idx = 0; backends[be_idx] != NULL; be_idx++) {
-- const char *be_dn = slapi_entry_get_dn_const(backends[be_idx]);
-- const char *be_name = slapi_entry_attr_get_ref(backends[be_idx], "cn");
-- if (!be_dn || !be_name) {
-- continue;
-- }
--
-- /* Check each attribute that should have integerOrderingMatch */
-- for (size_t attr_idx = 0; attrs_to_check[attr_idx] != NULL; attr_idx++) {
-- const char *attr_name = attrs_to_check[attr_idx];
-- struct slapi_pblock *idx_pb = slapi_pblock_new();
-- Slapi_Entry **idx_entries = NULL;
-- char *idx_dn = slapi_create_dn_string("cn=%s,cn=index,%s",
-- attr_name, be_dn);
-- char *idx_filter = "(objectclass=nsIndex)";
-- PRBool has_matching_rule = PR_FALSE;
--
-- if (!idx_dn) {
-- slapi_pblock_destroy(idx_pb);
-- continue;
-- }
--
-- slapi_search_internal_set_pb(
-- idx_pb, idx_dn,
-- LDAP_SCOPE_BASE,
-- idx_filter, NULL, 0, NULL, NULL,
-- plugin_get_default_component_id(), 0);
-- slapi_search_internal_pb(idx_pb);
-- slapi_pblock_get(idx_pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &idx_entries);
--
-- if (idx_entries && idx_entries[0]) {
-- /* Index exists, check if it has integerOrderingMatch */
-- Slapi_Attr *mr_attr = NULL;
-- if (slapi_entry_attr_find(idx_entries[0], "nsMatchingRule", &mr_attr) == 0) {
-- Slapi_Value *sval = NULL;
-- int idx;
-- for (idx = slapi_attr_first_value(mr_attr, &sval);
-- idx != -1;
-- idx = slapi_attr_next_value(mr_attr, idx, &sval)) {
-- const struct berval *bval = slapi_value_get_berval(sval);
-- if (bval && bval->bv_val &&
-- strcasecmp(bval->bv_val, "integerOrderingMatch") == 0) {
-- has_matching_rule = PR_TRUE;
-- break;
-- }
-- }
-- }
--
-- if (!has_matching_rule) {
-- /* Index exists but doesn't have integerOrderingMatch, log a warning */
-- slapi_log_err(SLAPI_LOG_ERR, "upgrade_check_id_index_matching_rule",
-- "Index '%s' in backend '%s' is missing 'nsMatchingRule: integerOrderingMatch'. "
-- "Incorrectly configured system indexes can lead to poor search performance, replication issues, and other operational problems. "
-- "To fix this, add the matching rule and reindex: "
-- "dsconf <instance> backend index set --add-mr integerOrderingMatch --attr %s %s && "
-- "dsconf <instance> backend index reindex --attr %s %s. "
-- "WARNING: Reindexing can be resource-intensive and may impact server performance on a live system. "
-- "Consider scheduling reindexing during maintenance windows or periods of low activity.\n",
-- attr_name, be_name, attr_name, be_name, attr_name, be_name);
-- }
-- }
--
-- slapi_ch_free_string(&idx_dn);
-- slapi_free_search_results_internal(idx_pb);
-- slapi_pblock_destroy(idx_pb);
-- }
-- }
-- }
--
-- slapi_free_search_results_internal(pb);
-- slapi_pblock_destroy(pb);
--
-- return uresult;
--}
-
- /*
- * Upgrade the base config of the PAM PTA plugin.
-@@ -879,10 +778,6 @@ upgrade_server(void)
- return UPGRADE_FAILURE;
- }
-
-- if (upgrade_check_id_index_matching_rule() != UPGRADE_SUCCESS) {
-- return UPGRADE_FAILURE;
-- }
--
- return UPGRADE_SUCCESS;
- }
-
-diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in
-index ae359ab6b..61a8cb368 100644
---- a/rpm/389-ds-base.spec.in
-+++ b/rpm/389-ds-base.spec.in
-@@ -650,9 +650,6 @@ for dir in "$instbase"/slapd-* ; do
- else
- echo "instance $inst is not running" >> "$output" 2>&1 || :
- fi
-- # Run index-check on all instances (running or not)
-- # This fixes index ordering mismatches from older versions
-- dsctl "$inst_name" index-check --fix >> "$output2" 2>&1 || :
- ninst=$((ninst + 1))
- done
-
-diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
-index f3dbe7c92..6c8cbc018 100644
---- a/src/lib389/lib389/backend.py
-+++ b/src/lib389/lib389/backend.py
-@@ -647,9 +647,10 @@ class Backend(DSLdapObject):
- # Default system indexes taken from ldap/servers/slapd/back-ldbm/instance.c
- # Note: entryrdn and ancestorid are internal system indexes that are not
- # exposed in cn=config - they are managed internally by the server.
-- # Only parentid has a DSE config entry (for the integerOrderingMatch rule).
-+ # parentid works correctly with both lexicographic and integer ordering,
-+ # so integerOrderingMatch is not required.
- expected_system_indexes = {
-- 'parentid': {'types': ['eq'], 'matching_rule': 'integerOrderingMatch'},
-+ 'parentid': {'types': ['eq'], 'matching_rule': None},
- 'objectClass': {'types': ['eq'], 'matching_rule': None},
- 'aci': {'types': ['pres'], 'matching_rule': None},
- 'nscpEntryDN': {'types': ['eq'], 'matching_rule': None},
-diff --git a/src/lib389/lib389/cli_ctl/dbtasks.py b/src/lib389/lib389/cli_ctl/dbtasks.py
-index cd96cdaf7..b02de203f 100644
---- a/src/lib389/lib389/cli_ctl/dbtasks.py
-+++ b/src/lib389/lib389/cli_ctl/dbtasks.py
-@@ -10,6 +10,7 @@
- import glob
- import os
- import re
-+import signal
- import subprocess
- from enum import Enum
- from lib389._constants import TaskWarning
-@@ -263,45 +264,53 @@ def _check_disk_ordering(db_dir, backend, index_name, dbscan_path, is_mdb, log):
- if not index_file:
- return IndexOrdering.UNKNOWN
-
-+ # Only read the first 100 lines from dbscan to avoid scanning the
-+ # entire index (which can take hours on large databases).
- try:
-- result = subprocess.run(
-+ proc = subprocess.Popen(
- [dbscan_path, "-f", index_file],
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- universal_newlines=True,
-- timeout=60,
- )
-
-- if result.returncode != 0:
-- log.warning(" dbscan returned non-zero exit code for %s", index_file)
-- return IndexOrdering.UNKNOWN
--
-- # Parse keys from dbscan output
- keys = []
-- for line in result.stdout.split("\n"):
-+ line_count = 0
-+ assert proc.stdout is not None
-+ for line in proc.stdout:
-+ line_count += 1
-+ if line_count > 100:
-+ break
- line = line.strip()
- if line.startswith("="):
- match = re.match(r"^=(\d+)", line)
- if match:
- keys.append(int(match.group(1)))
-
-+ proc.terminate()
-+ try:
-+ proc.wait(timeout=5)
-+ except subprocess.TimeoutExpired:
-+ proc.kill()
-+ proc.wait()
-+
-+ if proc.returncode not in (0, -signal.SIGTERM):
-+ log.warning(" dbscan returned non-zero exit code for %s", index_file)
-+ return IndexOrdering.UNKNOWN
-+
- if len(keys) < 2:
- return IndexOrdering.UNKNOWN
-
- # Check if keys are in integer order by looking for decreasing numeric values
- # (which would indicate lexicographic ordering, e.g., "3" < "30" < "4")
- prev_id = keys[0]
-- for i in range(1, min(len(keys), 100)):
-- current_id = keys[i]
-+ for current_id in keys[1:]:
- if prev_id > current_id:
- return IndexOrdering.LEXICOGRAPHIC
- prev_id = current_id
-
- return IndexOrdering.INTEGER
-
-- except subprocess.TimeoutExpired:
-- log.warning(" dbscan timed out for %s", index_file)
-- return IndexOrdering.UNKNOWN
- except OSError as e:
- log.warning(" Error running dbscan: %s", e)
- return IndexOrdering.UNKNOWN
-@@ -375,8 +384,7 @@ def dbtasks_index_check(inst, log, args):
-
- # Track all issues found
- all_ok = True
-- mismatches = [] # (backend, index_name) tuples needing reindex
-- missing_matching_rules = [] # (backend, index_name) tuples missing integerOrderingMatch
-+ config_fixes = [] # (backend, index_name, action) tuples: action is "add_mr" or "remove_mr"
- scan_limits_to_remove = [] # (backend, index_name) tuples with nsIndexIDListScanLimit
- ancestorid_configs_to_remove = [] # backend names with ancestorid config entries
- remove_ancestorid_from_defaults = False # Flag to remove from cn=default indexes
-@@ -409,13 +417,6 @@ def dbtasks_index_check(inst, log, args):
-
- if disk_ordering == IndexOrdering.UNKNOWN:
- log.info(" %s - could not determine disk ordering, skipping", index_name)
-- # For parentid, still check if matching rule is missing
-- if index_name == "parentid":
-- config_has_int_order = _has_integer_ordering_match(dse_ldif, backend, index_name)
-- if not config_has_int_order:
-- log.warning(" %s - missing integerOrderingMatch in config", index_name)
-- missing_matching_rules.append((backend, index_name))
-- all_ok = False
- continue
-
- config_has_int_order = _has_integer_ordering_match(dse_ldif, backend, index_name)
-@@ -423,18 +424,15 @@ def dbtasks_index_check(inst, log, args):
- log.info(" %s - config: %s, disk: %s",
- index_name, config_desc, disk_ordering.value)
-
-- # For parentid, the desired state is always integer ordering
-+ # Both orderings are valid for parentid, but config must match disk.
- if index_name == "parentid":
-- if not config_has_int_order:
-- log.warning(" %s - missing integerOrderingMatch in config", index_name)
-- if (backend, index_name) not in missing_matching_rules:
-- missing_matching_rules.append((backend, index_name))
-+ if config_has_int_order and disk_ordering == IndexOrdering.LEXICOGRAPHIC:
-+ log.warning(" %s - MISMATCH: config has integerOrderingMatch but disk is lexicographic", index_name)
-+ config_fixes.append((backend, index_name, "remove_mr"))
- all_ok = False
--
-- if disk_ordering == IndexOrdering.LEXICOGRAPHIC:
-- log.warning(" %s - disk ordering is lexicographic, needs reindex", index_name)
-- if (backend, index_name) not in mismatches:
-- mismatches.append((backend, index_name))
-+ elif not config_has_int_order and disk_ordering == IndexOrdering.INTEGER:
-+ log.warning(" %s - MISMATCH: config is lexicographic but disk has integer ordering", index_name)
-+ config_fixes.append((backend, index_name, "add_mr"))
- all_ok = False
-
- # Handle issues
-@@ -480,26 +478,27 @@ def dbtasks_index_check(inst, log, args):
- log.error(" Failed to remove ancestorid config from backend %s: %s", backend, e)
- return False
-
-- # Add missing matching rules to dse.ldif
-- for backend, index_name in missing_matching_rules:
-+ # Fix config-vs-disk ordering mismatches by adjusting config to match disk
-+ for backend, index_name, action in config_fixes:
- index_dn = "cn={},cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(
- index_name, backend
- )
-- log.info(" Adding integerOrderingMatch to %s in backend %s...", index_name, backend)
-- try:
-- dse_ldif.add(index_dn, "nsMatchingRule", "integerOrderingMatch")
-- log.info(" Updated dse.ldif with integerOrderingMatch for %s", index_name)
-- except Exception as e:
-- log.error(" Failed to update dse.ldif for %s: %s", index_name, e)
-- return False
--
-- # Reindex indexes with disk ordering issues
-- for backend, index_name in mismatches:
-- log.info(" Reindexing %s in backend %s...", index_name, backend)
-- if not inst.db2index(bename=backend, attrs=[index_name]):
-- log.error(" Failed to reindex %s", index_name)
-- return False
-- log.info(" Reindex of %s completed successfully", index_name)
-+ if action == "add_mr":
-+ log.info(" Adding integerOrderingMatch to %s in backend %s...", index_name, backend)
-+ try:
-+ dse_ldif.add(index_dn, "nsMatchingRule", "integerOrderingMatch")
-+ log.info(" Updated dse.ldif with integerOrderingMatch for %s", index_name)
-+ except Exception as e:
-+ log.error(" Failed to update dse.ldif for %s: %s", index_name, e)
-+ return False
-+ elif action == "remove_mr":
-+ log.info(" Removing integerOrderingMatch from %s in backend %s...", index_name, backend)
-+ try:
-+ dse_ldif.delete(index_dn, "nsMatchingRule", "integerOrderingMatch")
-+ log.info(" Removed integerOrderingMatch from %s", index_name)
-+ except Exception as e:
-+ log.error(" Failed to remove integerOrderingMatch from %s: %s", index_name, e)
-+ return False
-
- log.info("All issues fixed")
- return True
-@@ -563,5 +562,5 @@ def create_parser(subcommands):
- index_check_parser.add_argument('backend', nargs='?', default=None,
- help="Backend to check. If not specified, all backends are checked.")
- index_check_parser.add_argument('--fix', action='store_true', default=False,
-- help="Fix mismatches by reindexing affected indexes")
-+ help="Fix mismatches by adjusting config to match on-disk data")
- index_check_parser.set_defaults(func=dbtasks_index_check)
---
-2.53.0
-
diff --git a/0026-Security-fix-for-CVE-2025-14905.patch b/0026-Security-fix-for-CVE-2025-14905.patch
deleted file mode 100644
index 7469b61..0000000
--- a/0026-Security-fix-for-CVE-2025-14905.patch
+++ /dev/null
@@ -1,93 +0,0 @@
-From e2562f5894dd05a3b062e7820f471f2f8e12b85d Mon Sep 17 00:00:00 2001
-From: tbordaz <tbordaz@redhat.com>
-Date: Wed, 25 Feb 2026 14:06:42 +0100
-Subject: [PATCH] Security fix for CVE-2025-14905
-
-Description:
- A vulnerability was found in the 389 Directory Server.
- The 389 Directory Server present a risk of heap buffer overflow that
- can be exploited to excute a Denial of Service and potential Remote
- Code Execution
-
-References:
- - https://access.redhat.com/security/cve/CVE-2025-14905
- - https://bugzilla.redhat.com/show_bug.cgi?id=2423624
----
- ldap/servers/slapd/schema.c | 47 ++++++++++++++++++++++++++++++-------
- 1 file changed, 38 insertions(+), 9 deletions(-)
-
-diff --git a/ldap/servers/slapd/schema.c b/ldap/servers/slapd/schema.c
-index 9ef4ee4bf..7712a720d 100644
---- a/ldap/servers/slapd/schema.c
-+++ b/ldap/servers/slapd/schema.c
-@@ -1410,6 +1410,7 @@ schema_attr_enum_callback(struct asyntaxinfo *asip, void *arg)
- const char *attr_desc, *syntaxoid;
- char *outp, syntaxlengthbuf[128];
- int i;
-+ int nb_aliases = 0;
-
- vals[0] = &val;
-
-@@ -1435,6 +1436,7 @@ schema_attr_enum_callback(struct asyntaxinfo *asip, void *arg)
- if (asip->asi_aliases != NULL) {
- for (i = 0; asip->asi_aliases[i] != NULL; ++i) {
- aliaslen += strlen(asip->asi_aliases[i]);
-+ nb_aliases++;
- }
- }
-
-@@ -1452,15 +1454,42 @@ schema_attr_enum_callback(struct asyntaxinfo *asip, void *arg)
- * XXX: 256 is a magic number... it must be big enough to account for
- * all of the fixed sized items we output.
- */
-- sizedbuffer_allocate(aew->psbAttrTypes, 256 + strlen(asip->asi_oid) +
-- strlen(asip->asi_name) +
-- aliaslen + strlen_null_ok(attr_desc) +
-- strlen(syntaxoid) +
-- strlen_null_ok(asip->asi_superior) +
-- strlen_null_ok(asip->asi_mr_equality) +
-- strlen_null_ok(asip->asi_mr_ordering) +
-- strlen_null_ok(asip->asi_mr_substring) +
-- strcat_extensions(NULL, asip->asi_extensions));
-+ {
-+ int asi_oid_strlen = strlen(asip->asi_oid) + 8; /* "( %s NAME " */
-+ int asi_name_strlen = strlen(asip->asi_name) + 6; /* "( '%s' ...)" */
-+ int asi_aliases_strlen = aliaslen + nb_aliases * 3; /* "'%s' " */
-+ int asi_desc_strlen = strlen_null_ok(attr_desc) + 7; /* "DESC '%s'" */
-+ int asi_syntaxoid_strlen = strlen("SYNTAX ") + strlen(syntaxoid) + strlen(syntaxlengthbuf);
-+ int asi_superior_strlen = strlen("SUP ") + strlen_null_ok(asip->asi_superior);
-+ int asi_mr_equality_strlen = strlen("EQUALITY ") + strlen_null_ok(asip->asi_mr_equality);
-+ int asi_mr_ordering_strlen = strlen("ORDERING ") + strlen_null_ok(asip->asi_mr_ordering);
-+ int asi_mr_substring_strlen = strlen("SUBSTR ") + strlen_null_ok(asip->asi_mr_substring);
-+ int asi_flags_strlen = strlen("SINGLE-VALUE ") +
-+ strlen(schema_obsolete_with_spaces) +
-+ strlen(schema_collective_with_spaces) +
-+ strlen(schema_nousermod_with_spaces) +
-+ strlen("USAGE distributedOperation ") +
-+ strlen("USAGE dSAOperation ") +
-+ strlen("USAGE directoryOperation ");
-+ int asi_extension_strlen = strcat_extensions(NULL, asip->asi_extensions);
-+
-+ if (aew->enquote_sup_oc) {
-+ /* it enquote the syntax oid */
-+ asi_syntaxoid_strlen += 2;
-+ }
-+
-+ sizedbuffer_allocate(aew->psbAttrTypes, 256 + asi_oid_strlen +
-+ asi_name_strlen +
-+ asi_aliases_strlen +
-+ asi_desc_strlen +
-+ asi_syntaxoid_strlen +
-+ asi_superior_strlen +
-+ asi_mr_equality_strlen +
-+ asi_mr_ordering_strlen +
-+ asi_mr_substring_strlen +
-+ asi_extension_strlen +
-+ asi_flags_strlen);
-+ }
-
- /*
- * Overall strategy is to maintain a pointer to the next location in
---
-2.53.0
-
diff --git a/0027-Issue-7302-dblib-bdb2mdb-fails-on-F43-F43-upgrade-73.patch b/0027-Issue-7302-dblib-bdb2mdb-fails-on-F43-F43-upgrade-73.patch
deleted file mode 100644
index f0054f9..0000000
--- a/0027-Issue-7302-dblib-bdb2mdb-fails-on-F43-F43-upgrade-73.patch
+++ /dev/null
@@ -1,70 +0,0 @@
-From 3f5f32ef36cf2c797c9379eba1ba2d8b2fd9841b Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Fri, 6 Mar 2026 17:19:58 +0100
-Subject: [PATCH] Issue 7302 - dblib bdb2mdb fails on F43 -> F43 upgrade
- (#7303)
-
-Bug Description:
-`db->stat` is stubbed with `nothing()` which returns DB_SUCCESS without
-populating the stats output parameter. Both `bdb_get_page_count()` and
-`bdb_get_entries_count()` then dereference the NULL stats pointer,
-causing a segfault.
-
-Fix Descrption:
-Add NULL checks for the stats pointer after `db->stat()` calls in both
-`bdb_get_page_count()` and `bdb_get_entries_count()`.
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7302
-
-Reviewed by: @tbordaz, @progier389 (Thanks!)
----
- .../slapd/back-ldbm/db-bdb/bdb_layer.c | 20 +++++++++++++++----
- 1 file changed, 16 insertions(+), 4 deletions(-)
-
-diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c
-index 5c61e30b2..4e5f9fd98 100644
---- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c
-+++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c
-@@ -1788,11 +1788,17 @@ bdb_get_page_count(dbi_db_t *db, uint32_t *count)
- rc = ((DB*)db)->stat(db, (DB_TXN*)txn, (void *)&stats, 0);
- if (rc != 0) {
- slapi_log_err(SLAPI_LOG_ERR, "bdb_get_page_count",
-- "Failed to get bd statistics: db error - %d %s\n",
-+ "Failed to get db statistics: db error - %d %s\n",
- rc, db_strerror(rc));
- rc = DBI_RC_OTHER;
-+ *count = 0;
-+ } else if (stats == NULL) {
-+ slapi_log_err(SLAPI_LOG_INFO, "bdb_get_page_count",
-+ "Failed to get db statistics: stats is NULL, defaulting page count to 0\n");
-+ *count = 0;
-+ } else {
-+ *count = stats->bt_pagecnt;
- }
-- *count = rc ? 0 : stats->bt_pagecnt;
- slapi_ch_free((void **)&stats);
- return rc;
- }
-@@ -7084,11 +7090,17 @@ bdb_get_entries_count(dbi_db_t *db, dbi_txn_t *txn, int *count)
- rc = ((DB*)db)->stat(db, (DB_TXN*)txn, (void *)&stats, 0);
- if (rc != 0) {
- slapi_log_err(SLAPI_LOG_ERR, "bdb_get_entries_count",
-- "Failed to get bd statistics: db error - %d %s\n",
-+ "Failed to get db statistics: db error - %d %s\n",
- rc, db_strerror(rc));
- rc = DBI_RC_OTHER;
-+ *count = 0;
-+ } else if (stats == NULL) {
-+ slapi_log_err(SLAPI_LOG_INFO, "bdb_get_entries_count",
-+ "Failed to get db statistics: stats is NULL, defaulting entries count to 0\n");
-+ *count = 0;
-+ } else {
-+ *count = stats->bt_ndata;
- }
-- *count = rc ? 0 : stats->bt_ndata;
- slapi_ch_free((void **)&stats);
- return rc;
- }
---
-2.53.0
-
diff --git a/0028-Issue-7267-MDB_BAD_VALSIZE-error-when-updating-index.patch b/0028-Issue-7267-MDB_BAD_VALSIZE-error-when-updating-index.patch
deleted file mode 100644
index d1e9ec6..0000000
--- a/0028-Issue-7267-MDB_BAD_VALSIZE-error-when-updating-index.patch
+++ /dev/null
@@ -1,681 +0,0 @@
-From 4809c6cf43ac9709568e2637e127f5a76c9d845e Mon Sep 17 00:00:00 2001
-From: progier389 <progier@redhat.com>
-Date: Wed, 25 Feb 2026 18:00:24 +0100
-Subject: [PATCH] Issue 7267 - MDB_BAD_VALSIZE error when updating index
- (#7268)
-
-* Issue 7267 - MDB_BAD_VALSIZE error when updating index
-* Improve import log when writer fails
-* Fix Sourcery AI comments
-* Fix INDEX_KEY_LENGTH typo
-
-Problem with the key prefix handling when key is too long and must be hashed.
-The issue is that the # that is prepended is not reset when iterating over the valueset values (Ending up with very long prefix)
-
-Also refactored the code to avoid duplicate the code that prepare the key from the attribute value (used when updating the index or retrieving a value from an index)
-
-Issue: #7267
-
-Reviewed by: @tbordaz , @vashirov (Thanks!)
-
-Co-authored-by: Viktor Ashirov <vashirov@redhat.com>
-
----------
-
-Co-authored-by: Viktor Ashirov <vashirov@redhat.com>
----
- .../tests/suites/indexes/regression_test.py | 85 +++++++++
- ldap/servers/slapd/back-ldbm/attrcrypt.h | 2 +-
- ldap/servers/slapd/back-ldbm/back-ldbm.h | 2 +
- .../slapd/back-ldbm/db-bdb/bdb_import.c | 39 +----
- .../back-ldbm/db-mdb/mdb_import_threads.c | 46 ++++-
- ldap/servers/slapd/back-ldbm/index.c | 161 +++++++-----------
- ldap/servers/slapd/back-ldbm/ldbm_attrcrypt.c | 13 +-
- .../servers/slapd/back-ldbm/proto-back-ldbm.h | 2 +-
- ldap/servers/slapd/log.c | 43 +++++
- ldap/servers/slapd/slapi-private.h | 2 +
- 10 files changed, 251 insertions(+), 144 deletions(-)
-
-diff --git a/dirsrvtests/tests/suites/indexes/regression_test.py b/dirsrvtests/tests/suites/indexes/regression_test.py
-index 71670de87..39f63e349 100644
---- a/dirsrvtests/tests/suites/indexes/regression_test.py
-+++ b/dirsrvtests/tests/suites/indexes/regression_test.py
-@@ -900,6 +900,91 @@ def test_concurrent_modifications_during_indexing(topo, homeDirectory_index_clea
- user.delete()
-
-
-+def test_idl_range_limit(topo, add_some_entries):
-+ """Test nsslapd-rangelookthroughlimit and AND shortcut
-+
-+ :id: 746a5af2-d755-11f0-8b62-c85309d5c3e3
-+ :setup: Standalone Instance with some entries
-+ :steps:
-+ 1. Set nsslapd-rangelookthroughlimit
-+ 2. Open a new connection and bound it as an user
-+ 3. Perform Range search that hit nsslapd-rangelookthroughlimit and AND shortcut
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ """
-+
-+ inst = topo.standalone
-+ added_entries = add_some_entries
-+
-+ dbconfig = LDBMConfig(inst)
-+ dbconfig.set('nsslapd-rangelookthroughlimit', "50")
-+
-+ conn = added_entries[0].bind(PW_DM)
-+
-+ entries = conn.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(&(objectclass=nsOrgPerson)(sn=foo)(uid>=a))")
-+ assert len(entries) == 3
-+
-+
-+def test_large_multivalued_sn_attribute(topo):
-+ """Test adding a user entry with 512 values for sn attribute, each 512 bytes
-+
-+ :id: 8f2a9b3c-e8d7-11ef-9a5f-482ae39447e5
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Create a user with 512 sn values, each 512 bytes long
-+ 2. Verify the user was created successfully
-+ 3. Search for the user and verify all sn values are present
-+ 4. Clean up the user entry
-+ :expectedresults:
-+ 1. User is created successfully
-+ 2. User entry exists
-+ 3. All 512 sn values are present and have correct length
-+ 4. User is deleted successfully
-+ """
-+
-+ inst = topo.standalone
-+ users = UserAccounts(inst, DEFAULT_SUFFIX)
-+
-+ log.info("Creating user with 512 sn values, each 512 bytes")
-+
-+ # Generate 512 unique sn values, each 512 bytes long
-+ # Use a pattern that makes each value unique but predictable
-+ sn_values = []
-+ for i in range(512):
-+ # Create a 512-byte value with unique identifier at the start
-+ value = f'sn_value_{i:04d}_' + 'x' * (512 - len(f'sn_value_{i:04d}_'))
-+ sn_values.append(value)
-+
-+ # Create the user with first sn value
-+ user_name = 'test_user_large_sn'
-+ user = users.create(properties={
-+ 'uid': user_name,
-+ 'cn': user_name,
-+ 'sn': sn_values,
-+ 'uidNumber': '99999',
-+ 'gidNumber': '99999',
-+ 'homeDirectory': f'/home/{user_name}'
-+ })
-+
-+ # Verify the entry was created and has all sn values
-+ log.info("Verifying all sn values are present")
-+ sn_attr_values = user.get_attr_vals_utf8('sn')
-+
-+ assert len(sn_attr_values) == 512, f"Expected 512 sn values, got {len(sn_attr_values)}"
-+
-+ # Verify each value has the correct length
-+ for idx, value in enumerate(sn_attr_values):
-+ assert len(value) == 512, f"sn value {idx} has length {len(value)}, expected 512"
-+
-+ log.info("Successfully created and verified user with 512 sn values of 512 bytes each")
-+
-+ # Clean up
-+ user.delete()
-+ log.info("User entry deleted successfully")
-+
-+
- if __name__ == "__main__":
- # Run isolated
- # -s for DEBUG mode
-diff --git a/ldap/servers/slapd/back-ldbm/attrcrypt.h b/ldap/servers/slapd/back-ldbm/attrcrypt.h
-index d653ba951..dcbea80fe 100644
---- a/ldap/servers/slapd/back-ldbm/attrcrypt.h
-+++ b/ldap/servers/slapd/back-ldbm/attrcrypt.h
-@@ -10,7 +10,7 @@
- #include <config.h>
- #endif
-
--/* Private tructures and #defines used in the attribute encryption code. */
-+/* Private structures and #defines used in the attribute encryption code. */
-
- #ifndef _ATTRCRYPT_H_
- #define _ATTRCRYPT_H_
-diff --git a/ldap/servers/slapd/back-ldbm/back-ldbm.h b/ldap/servers/slapd/back-ldbm/back-ldbm.h
-index e23e7ff43..92aa1ddbb 100644
---- a/ldap/servers/slapd/back-ldbm/back-ldbm.h
-+++ b/ldap/servers/slapd/back-ldbm/back-ldbm.h
-@@ -104,6 +104,8 @@ typedef unsigned short u_int16_t;
- */
- #define BE_CHANGELOG_FILE "replication_changelog"
-
-+#define INDEX_KEY_LENGTH(lenval,lenprefix) (lenval+lenprefix+2)
-+
- #define BDB_IMPL "bdb"
- #define BDB_BACKEND "libback-ldbm" /* This backend plugin */
- #define BDB_NEWIDL "newidl" /* new idl format */
-diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c
-index a6cb10aec..489433801 100644
---- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c
-+++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c
-@@ -75,9 +75,9 @@ static IDList *bdb_idl_union_allids(backend *be, struct attrinfo *ai, IDList *a,
- #define DEBUG_SUBCOUNT_MSG(msg, ...) { debug_subcount(__FUNCTION__, __LINE__, (msg), __VA_ARGS__); }
- #define DUMP_SUBCOUNT_KEY(msg, key, ret) { debug_subcount(__FUNCTION__, __LINE__, "ret=%d size=%u ulen=%u doff=%u dlen=%u", \
- ret, (key).size, (key).ulen, (key).doff, (key).dlen); \
-- if (ret == 0) hexadump(msg, (key).data, 0, (key).size); \
-+ if (ret == 0) slapi_log_hexadump(SLAPI_LOG_INFO, msg, (key).data, (key).size); \
- else if (ret == DB_BUFFER_SMALL) \
-- hexadump(msg, (key).data, 0, (key).ulen); }
-+ slapi_log_hexadump(SLAPI_LOG_INFO, msg, (key).data, (key).ulen); }
-
- static void
- debug_subcount(const char *funcname, int line, char *msg, ...)
-@@ -90,41 +90,6 @@ debug_subcount(const char *funcname, int line, char *msg, ...)
- slapi_log_err(SLAPI_LOG_INFO, (char*)funcname, "DEBUG SUBCOUNT [%d] %s\n", line, buff);
- }
-
--/*
-- * Dump a memory buffer in hexa and ascii in error log
-- *
-- * addr - The memory buffer address.
-- * len - The memory buffer lenght.
-- */
--static void
--hexadump(char *msg, const void *addr, size_t offset, size_t len)
--{
--#define HEXADUMP_TAB 4
--/* 4 characters per bytes: 2 hexa digits, 1 space and the ascii */
--#define HEXADUMP_BUF_SIZE (4*16+HEXADUMP_TAB)
-- char hexdigit[] = "0123456789ABCDEF";
--
-- const unsigned char *pt = addr;
-- char buff[HEXADUMP_BUF_SIZE+1];
-- memset (buff, ' ', HEXADUMP_BUF_SIZE);
-- buff[HEXADUMP_BUF_SIZE] = '\0';
-- while (len > 0) {
-- int dpl;
-- for (dpl = 0; dpl < 16 && len>0; dpl++, len--) {
-- buff[3*dpl] = hexdigit[((*pt) >> 4) & 0xf];
-- buff[3*dpl+1] = hexdigit[(*pt) & 0xf];
-- buff[3*16+HEXADUMP_TAB+dpl] = (*pt>=0x20 && *pt<0x7f) ? *pt : '.';
-- pt++;
-- }
-- for (;dpl < 16; dpl++) {
-- buff[3*dpl] = ' ';
-- buff[3*dpl+1] = ' ';
-- buff[3*16+HEXADUMP_TAB+dpl] = ' ';
-- }
-- slapi_log_err(SLAPI_LOG_INFO, msg, "[0x%08lx] %s\n", offset, buff);
-- offset += 16;
-- }
--}
- #else
- #define DEBUG_SUBCOUNT_MSG(msg, ...)
- #define DUMP_SUBCOUNT_KEY(msg, key, ret)
-diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c
-index 2270abd69..bfb902c02 100644
---- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c
-+++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c
-@@ -1122,6 +1122,21 @@ dbmdb_import_entry_info_by_backentry(mdb_privdb_t *db, BulkQueueData_t *bqdata,
- return dnrc;
- }
-
-+/* Log wqelmt details */
-+void
-+log_wqelmt(int loglvl, char *fname, WorkerQueueData_t *wqelmt)
-+{
-+ if (wqelmt->dn) {
-+ slapi_log_err(loglvl, fname, "log_wqelmt: dn=%s\n", wqelmt->dn);
-+ }
-+ if (wqelmt->filename && wqelmt->lineno) {
-+ slapi_log_err(loglvl, fname, "log_wqelmt: ldif=%s[%d]\n", wqelmt->filename, wqelmt->lineno);
-+ }
-+ if (wqelmt->data) {
-+ size_t len = wqelmt->datalen ? wqelmt->datalen : strlen(wqelmt->data);
-+ slapi_log_hexadump(loglvl, "log_wqelmt:data", wqelmt->data, len);
-+ }
-+}
-
- /* producer thread for ldif import case:
- * read through the given file list, parsing entries (str2entry), assigning
-@@ -1254,6 +1269,7 @@ dbmdb_import_producer(void *param)
- import_log_notice(job, SLAPI_LOG_ERR, "dbmdb_import_producer",
- "ns_slapd software error: unexpected dbmdb_import_entry_info return code: %d.",
- wqelmt.dnrc);
-+ log_wqelmt(SLAPI_LOG_ERR, "dbmdb_import_producer", &wqelmt);
- abort();
- case DNRC_OK:
- case DNRC_SUFFIX:
-@@ -1757,6 +1773,7 @@ dbmdb_index_producer(void *param)
- import_log_notice(job, SLAPI_LOG_ERR, "dbmdb_index_producer",
- "ns_slapd software error: unexpected dbmdb_import_entry_info return code: %d.",
- tmpslot.dnrc);
-+ log_wqelmt(SLAPI_LOG_ERR, "dbmdb_index_producer", &tmpslot);
- abort();
- case DNRC_OK:
- case DNRC_SUFFIX:
-@@ -3934,10 +3951,24 @@ dbmdb_import_writer(void*param)
- if (!txn) {
- MDB_STAT_STEP(stats, MDB_STAT_TXNSTART);
- rc = TXN_BEGIN(ctx->ctx->env, NULL, 0, &txn);
-+ if (rc) {
-+ slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_writer",
-+ "Failed to begin a txn. Error is 0x%x: %s.\n",
-+ rc, mdb_strerror(rc));
-+ }
- }
- if (!rc) {
- MDB_STAT_STEP(stats, MDB_STAT_WRITE);
- rc = MDB_PUT(txn, slot->dbi->dbi, &slot->key, &slot->data, 0);
-+ if (rc) {
-+ slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_writer",
-+ "Failed to write record in dbi %s. Error is 0x%x: %s.\n",
-+ slot->dbi->dbname, rc, mdb_strerror(rc));
-+ slapi_log_hexadump(SLAPI_LOG_ERR, "dbmdb_import_writer:key",
-+ slot->key.mv_data, slot->key.mv_size);
-+ slapi_log_hexadump(SLAPI_LOG_ERR, "dbmdb_import_writer:data",
-+ slot->data.mv_data, slot->data.mv_size);
-+ }
- }
- MDB_STAT_STEP(stats, MDB_STAT_RUN);
- nextslot = slot->next;
-@@ -3951,6 +3982,9 @@ dbmdb_import_writer(void*param)
- rc = TXN_COMMIT(txn);
- MDB_STAT_STEP(stats, MDB_STAT_RUN);
- if (rc) {
-+ slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_writer",
-+ "Failed to commit the txn. Error is 0x%x: %s.\n",
-+ rc, mdb_strerror(rc));
- break;
- }
- count = 0;
-@@ -3963,6 +3997,10 @@ dbmdb_import_writer(void*param)
- MDB_STAT_STEP(stats, MDB_STAT_RUN);
- if (!rc) {
- txn = NULL;
-+ } else {
-+ slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_writer",
-+ "Failed to commit the txn. Error is 0x%x: %s.\n",
-+ rc, mdb_strerror(rc));
- }
- }
- if (txn) {
-@@ -3975,13 +4013,17 @@ dbmdb_import_writer(void*param)
- if (!rc) {
- /* Ensure that all data are written on disk */
- rc = mdb_env_sync(ctx->ctx->env, 1);
-+ if (rc) {
-+ slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_writer",
-+ "mdb_env_sync failed. Error is 0x%x: %s.\n",
-+ rc, mdb_strerror(rc));
-+ }
- }
- MDB_STAT_END(stats);
-
- if (rc) {
- slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_writer",
-- "Failed to write in the database. Error is 0x%x: %s.\n",
-- rc, mdb_strerror(rc));
-+ "Aborting import after failure.\n");
- thread_abort(info);
- } else {
- char buf[200];
-diff --git a/ldap/servers/slapd/back-ldbm/index.c b/ldap/servers/slapd/back-ldbm/index.c
-index a5004be19..c108bce3c 100644
---- a/ldap/servers/slapd/back-ldbm/index.c
-+++ b/ldap/servers/slapd/back-ldbm/index.c
-@@ -881,6 +881,67 @@ index_read(
- return index_read_ext(be, (char *)type, indextype, val, txn, err, NULL);
- }
-
-+/* Prepare an index key (hashed if too long, encrypted if needed from attribute value */
-+int
-+prepare_key(backend *be, struct attrinfo *a, char **buf, size_t *buflen,
-+ int flags, const char *prefix, const struct berval *bvp, dbi_val_t *key)
-+{
-+ /* Key format is [Hash?] [prefix] [val] [\0] */
-+ struct ldbminfo *li = (struct ldbminfo *)be->be_database->plg_private;
-+ size_t plen = strlen(prefix);
-+ struct berval *hashed_bvp = NULL;
-+ struct berval *encrypted_bvp = NULL;
-+ int rc = 0;
-+
-+ /* Hash large index key if necessary */
-+ if (INDEX_KEY_LENGTH(bvp->bv_len,plen) >= li->li_max_key_len) {
-+ rc = attrcrypt_hash_large_index_key(be, prefix, a, bvp, &hashed_bvp);
-+ if (rc) {
-+ slapi_log_err(SLAPI_LOG_ERR, "index_read_ext_allids",
-+ "Failed to hash large index key for %s\n", a->ai_type);
-+ return rc;
-+ } else {
-+ bvp = hashed_bvp;
-+ }
-+ }
-+
-+ /* Encrypt the index key if necessary */
-+ if (rc == 0 && a->ai_attrcrypt && (0 == (flags & BE_INDEX_DONT_ENCRYPT))) {
-+ rc = attrcrypt_encrypt_index_key(be, a, bvp, &encrypted_bvp);
-+ if (rc) {
-+ slapi_log_err(SLAPI_LOG_ERR, "addordel_values_sv",
-+ "Failed to encrypt index key for %s\n", a->ai_type);
-+ } else {
-+ bvp = encrypted_bvp;
-+ }
-+ }
-+ if (hashed_bvp) {
-+ prefix = slapi_ch_smprintf("%c%s",HASH_PREFIX, prefix);
-+ plen++;
-+ }
-+ if (buf && buflen) {
-+ if (plen+bvp->bv_len+1 > *buflen) {
-+ *buflen = plen+bvp->bv_len+1;
-+ *buf = slapi_ch_realloc(*buf, *buflen);
-+ }
-+ dblayer_value_concat(be, key, *buf, *buflen, prefix, plen, bvp->bv_val, bvp->bv_len, "", 1);
-+ } else {
-+ dblayer_value_concat(be, key, NULL, 0, prefix, plen, bvp->bv_val, bvp->bv_len, "", 1);
-+ }
-+
-+ if (hashed_bvp) {
-+ ber_bvfree(hashed_bvp);
-+ hashed_bvp = NULL;
-+ slapi_ch_free_string((char**)&prefix);
-+ }
-+ if (encrypted_bvp) {
-+ ber_bvfree(encrypted_bvp);
-+ encrypted_bvp = NULL;
-+ }
-+ return rc;
-+}
-+
-+
- /*
- * Extended version of index_read.
- * The unindexed flag can be used to distinguish between a
-@@ -917,7 +978,6 @@ index_read_ext_allids(
- struct berval *hashed_val = NULL;
- int is_and = 0;
- unsigned int ai_flags = 0;
-- struct ldbminfo *li = (struct ldbminfo *)be->be_database->plg_private;
-
- *err = 0;
-
-@@ -1028,36 +1088,7 @@ index_read_ext_allids(
- }
-
- if (val != NULL) {
-- size_t vlen;
-- int ret = 0;
--
-- /* If necessary, hash this index key */
-- if (val->bv_len >= li->li_max_key_len) {
-- ret = attrcrypt_hash_large_index_key(be, &prefix, ai, val, &hashed_val);
-- if (ret) {
-- slapi_log_err(SLAPI_LOG_ERR, "index_read_ext_allids",
-- "Failed to hash large index key for %s\n", basetype);
-- *err = DBI_RC_OTHER;
-- index_free_prefix(prefix);
-- slapi_ch_free_string(&basetmp);
-- return (NULL);
-- }
-- if (hashed_val) {
-- val = hashed_val;
-- }
-- }
-- /* If necessary, encrypt this index key */
-- ret = attrcrypt_encrypt_index_key(be, ai, val, &encrypted_val);
-- if (ret) {
-- slapi_log_err(SLAPI_LOG_ERR, "index_read_ext_allids",
-- "Failed to encrypt index key for %s\n", basetype);
-- }
-- if (encrypted_val) {
-- val = encrypted_val;
-- }
-- vlen = val->bv_len;
-- dblayer_value_concat(be, &key, buf, sizeof(buf),
-- prefix, strlen(prefix), val->bv_val, vlen, "", 1);
-+ (void) prepare_key(be, ai, NULL, 0, 0, prefix, val, &key);
- } else {
- dblayer_value_concat(be, &key, buf, sizeof(buf), prefix, strlen(prefix),
- "", 1, NULL, 0);
-@@ -1824,6 +1855,7 @@ index_range_read(
- return index_range_read_ext(pb, be, type, indextype, operator, val, nextval, range, txn, err, 0);
- }
-
-+
- static int
- addordel_values_sv(
- backend *be,
-@@ -1842,15 +1874,10 @@ addordel_values_sv(
- int i = 0;
- dbi_val_t key = {0};
- dbi_txn_t *db_txn = NULL;
-- size_t plen, vlen, len;
- char *tmpbuf = NULL;
- size_t tmpbuflen = 0;
-- char *realbuf;
- char *prefix = NULL;
- const struct berval *bvp;
-- struct berval *hashed_bvp = NULL;
-- struct berval *encrypted_bvp = NULL;
-- struct ldbminfo *li = (struct ldbminfo *)be->be_database->plg_private;
- char *index_id = get_index_name(be, db, a);
-
- slapi_log_err(SLAPI_LOG_TRACE, "addordel_values_sv", "%s_values\n",
-@@ -1889,66 +1916,14 @@ addordel_values_sv(
- return (rc);
- }
-
-- plen = strlen(prefix);
- for (i = 0; vals[i] != NULL; i++) {
- bvp = slapi_value_get_berval(vals[i]);
-
-- /* Hash large index key if necessary */
-- if (bvp->bv_len >= li->li_max_key_len) {
-- rc = attrcrypt_hash_large_index_key(be, &prefix, a, bvp, &hashed_bvp);
-- if (rc) {
-- slapi_log_err(SLAPI_LOG_ERR, "index_read_ext_allids",
-- "Failed to hash large index key for %s\n", a->ai_type);
-- break;
-- } else {
-- bvp = hashed_bvp;
-- plen = strlen(prefix);
-- }
-- }
-- /* Encrypt the index key if necessary */
-- {
-- if (a->ai_attrcrypt && (0 == (flags & BE_INDEX_DONT_ENCRYPT))) {
-- rc = attrcrypt_encrypt_index_key(be, a, bvp, &encrypted_bvp);
-- if (rc) {
-- slapi_log_err(SLAPI_LOG_ERR, "addordel_values_sv",
-- "Failed to encrypt index key for %s\n", a->ai_type);
-- } else {
-- bvp = encrypted_bvp;
-- }
-- }
-+ rc = prepare_key(be, a, &tmpbuf, &tmpbuflen, flags, prefix, bvp, &key);
-+ if (rc) {
-+ break;
- }
-
-- vlen = bvp->bv_len;
-- len = plen + vlen;
--
-- if (len < tmpbuflen) {
-- realbuf = tmpbuf;
-- } else {
-- tmpbuf = slapi_ch_realloc(tmpbuf, len + 1);
-- tmpbuflen = len + 1;
-- realbuf = tmpbuf;
-- }
--
-- assert(realbuf); /* For coverity */
-- memcpy(realbuf, prefix, plen);
-- memcpy(realbuf + plen, bvp->bv_val, vlen);
-- realbuf[len] = '\0';
-- /* Free the encrypted berval if necessary */
-- if (hashed_bvp) {
-- ber_bvfree(hashed_bvp);
-- hashed_bvp = NULL;
-- }
-- if (encrypted_bvp) {
-- ber_bvfree(encrypted_bvp);
-- encrypted_bvp = NULL;
-- }
-- /* should be okay to use USERMEM here because we know what
-- * the key is and it should never return a different value
-- * than the one we pass in.
-- */
-- dblayer_value_set_buffer(be, &key, realbuf, plen + vlen + 1);
-- key.ulen = tmpbuflen;
--
- if (slapi_is_loglevel_set(LDAP_DEBUG_TRACE)) {
- char encbuf[BUFSIZ];
-
-@@ -1981,10 +1956,6 @@ addordel_values_sv(
- ldbm_nasty(NASTY_MSG("addordel_values_sv"), index_id, 1130, rc);
- break;
- }
-- if (NULL != key.dptr && realbuf != key.dptr) { /* realloc'ed */
-- tmpbuf = key.dptr;
-- tmpbuflen = key.size;
-- }
- }
- index_free_prefix(prefix);
- if (tmpbuf != NULL) {
-diff --git a/ldap/servers/slapd/back-ldbm/ldbm_attrcrypt.c b/ldap/servers/slapd/back-ldbm/ldbm_attrcrypt.c
-index 7bb57ca66..7cf30053c 100644
---- a/ldap/servers/slapd/back-ldbm/ldbm_attrcrypt.c
-+++ b/ldap/servers/slapd/back-ldbm/ldbm_attrcrypt.c
-@@ -1069,15 +1069,15 @@ attrcrypt_decrypt_index_key(backend *be,
- * : NULL - no hash or failure
- */
- int
--attrcrypt_hash_large_index_key(backend *be, char **prefix, struct attrinfo *ai, const struct berval *in, struct berval **out)
-+attrcrypt_hash_large_index_key(backend *be, const char *prefix, struct attrinfo *ai, const struct berval *in, struct berval **out)
- {
- int ret = 0;
- struct berval *out_berval = NULL;
- struct ldbminfo *li = (struct ldbminfo *)be->be_database->plg_private;
-- char *new_prefix;
-+ size_t final_key_len = INDEX_KEY_LENGTH(in->bv_len, strlen(prefix));
-
- /* If the index key is too long (i.e mdb case) we must hash it */
-- if (in->bv_len >= li->li_max_key_len) {
-+ if (final_key_len >= li->li_max_key_len) {
- PK11Context *c = PK11_CreateDigestContext(SEC_OID_MD5);
- if (c != NULL) {
- unsigned char hash[32];
-@@ -1091,16 +1091,13 @@ attrcrypt_hash_large_index_key(backend *be, char **prefix, struct attrinfo *ai,
- return ENOMEM;
- }
- slapi_log_err(SLAPI_LOG_TRACE, "attrcrypt_hash_large_index_key",
-- "Key lenght (%lu) >= max key lenght (%lu) so key must be hashed\n", in->bv_len, li->li_max_key_len);
-+ "Key lenght (%lu) >= max key lenght (%lu) so key must be hashed\n", final_key_len, li->li_max_key_len);
- slapi_be_set_flag(be, SLAPI_BE_FLAG_DONT_BYPASS_FILTERTEST);
- PK11_DigestBegin(c);
- /* Compute hash for the key without the prefix */
- PK11_DigestOp(c, (unsigned char *)in->bv_val, in->bv_len);
- PK11_DigestFinal(c, hash, &hashLen, sizeof hash);
-- /* Add HASH_PREFIX before the prefix */
-- new_prefix = slapi_ch_smprintf("%c%s", HASH_PREFIX, *prefix);
-- index_free_prefix(*prefix);
-- *prefix = new_prefix;
-+
- /* Build the key: hash value in hexa */
- hkey = slapi_ch_malloc(1+2*sizeof hash);
- out_berval->bv_val = hkey;
-diff --git a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
-index 30a7aa11f..c882dac7b 100644
---- a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
-+++ b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
-@@ -622,7 +622,7 @@ int attrcrypt_encrypt_entry_inplace(backend *be, const struct backentry *inout);
- int attrcrypt_encrypt_entry(backend *be, const struct backentry *in, struct backentry **out);
- int attrcrypt_encrypt_index_key(backend *be, struct attrinfo *ai, const struct berval *in, struct berval **out);
- int attrcrypt_decrypt_index_key(backend *be, struct attrinfo *ai, const struct berval *in, struct berval **out);
--int attrcrypt_hash_large_index_key(backend *be, char **prefix, struct attrinfo *ai, const struct berval *in, struct berval **out);
-+int attrcrypt_hash_large_index_key(backend *be, const char *prefix, struct attrinfo *ai, const struct berval *in, struct berval **out);
- int attrcrypt_init(ldbm_instance *li);
- int attrcrypt_cleanup_private(ldbm_instance *li);
-
-diff --git a/ldap/servers/slapd/log.c b/ldap/servers/slapd/log.c
-index 80c07382a..93101494b 100644
---- a/ldap/servers/slapd/log.c
-+++ b/ldap/servers/slapd/log.c
-@@ -93,6 +93,10 @@ static int slapi_log_map[] = {
- #define FLUSH PR_TRUE
- #define NO_FLUSH PR_FALSE
-
-+#define HEXADUMP_TAB 4
-+/* 4 characters per bytes: 2 hexa digits, 1 space and the ascii */
-+#define HEXADUMP_BUF_SIZE (4*16+HEXADUMP_TAB)
-+
- /**************************************************************************
- * PROTOTYPES
- *************************************************************************/
-@@ -3133,6 +3137,45 @@ slapi_log_backtrace(int loglevel)
- }
- }
-
-+/*
-+ * Dump a memory buffer in hexa and ascii in error log
-+ *
-+ * addr - The memory buffer address.
-+ * len - The memory buffer lenght.
-+ */
-+void
-+slapi_log_hexadump(int loglevel, char *fname, const void *addr, size_t len)
-+{
-+ char hexdigit[] = "0123456789ABCDEF";
-+ const unsigned char *pt = addr;
-+ char buff[HEXADUMP_BUF_SIZE+1];
-+ size_t offset = 0;
-+
-+ if (!slapi_is_loglevel_set(loglevel)) {
-+ return;
-+ }
-+ memset (buff, ' ', HEXADUMP_BUF_SIZE);
-+ buff[HEXADUMP_BUF_SIZE] = '\0';
-+ while (len > 0) {
-+ int dpl;
-+ for (dpl = 0; dpl < 16 && len>0; dpl++, len--) {
-+ buff[3*dpl] = hexdigit[((*pt) >> 4) & 0xf];
-+ buff[3*dpl+1] = hexdigit[(*pt) & 0xf];
-+ buff[3*16+HEXADUMP_TAB+dpl] = (*pt>=0x20 && *pt<0x7f) ? *pt : '.';
-+ pt++;
-+ }
-+ for (;dpl < 16; dpl++) {
-+ buff[3*dpl] = ' ';
-+ buff[3*dpl+1] = ' ';
-+ buff[3*16+HEXADUMP_TAB+dpl] = ' ';
-+ }
-+ slapi_log_err(loglevel, fname, "[0x%08lx] %s\n", offset, buff);
-+ offset += 16;
-+ }
-+}
-+
-+
-+
- /******************************************************************************
- * write in the access log
- ******************************************************************************/
-diff --git a/ldap/servers/slapd/slapi-private.h b/ldap/servers/slapd/slapi-private.h
-index 72f4cd6f0..2da37ff6e 100644
---- a/ldap/servers/slapd/slapi-private.h
-+++ b/ldap/servers/slapd/slapi-private.h
-@@ -1527,6 +1527,8 @@ void slapi_pblock_set_task_warning(Slapi_PBlock *pb, task_warning warning);
- int slapi_exists_or_add_internal(Slapi_DN *dn, const char *filter, const char *entry, const char *modifier_name);
-
- void slapi_log_backtrace(int loglevel);
-+void slapi_log_hexadump(int loglevel, char *fname, const void *addr, size_t len);
-+
-
- /*
- * accesslog.c
---
-2.54.0
-
diff --git a/0029-Issue-7503-CVE-2026-9064-Add-a-limit-to-the-number-c.patch b/0029-Issue-7503-CVE-2026-9064-Add-a-limit-to-the-number-c.patch
deleted file mode 100644
index 3804c34..0000000
--- a/0029-Issue-7503-CVE-2026-9064-Add-a-limit-to-the-number-c.patch
+++ /dev/null
@@ -1,439 +0,0 @@
-From 7e9647f5bb5c47602f4cdf0022cf6bd22872d3ef Mon Sep 17 00:00:00 2001
-From: Mark Reynolds <mreynolds@redhat.com>
-Date: Thu, 21 May 2026 09:17:39 -0400
-Subject: [PATCH] Issue 7503 - CVE-2026-9064 - Add a limit to the number
- controls per operation
-
-Description:
-
-Security fix for CVE-2026-9064
-
-To prevent resource starvation limit the number of controls the server will
-process per operation. Reject the operation if number of controls exceeds
-the limit
-
-relates: https://github.com/389ds/389-ds-base/issues/7503
-
-References:
- - https://access.redhat.com/security/cve/cve-2026-9064
- - https://bugzilla.redhat.com/show_bug.cgi?id=2480093
-
-CI test assisted by: Cursor
-
-Reviewed by: jchapman & tbordaz (Thanks!!)
----
- .../suites/features/ldap_controls_test.py | 76 ++++++++++++++++++-
- ldap/schema/01core389.ldif | 3 +-
- ldap/servers/slapd/control.c | 14 +++-
- ldap/servers/slapd/libglobs.c | 52 +++++++++++++
- ldap/servers/slapd/proto-slap.h | 2 +
- ldap/servers/slapd/slap.h | 4 +
- .../389-console/src/lib/server/tuning.jsx | 52 ++++++++++++-
- 7 files changed, 197 insertions(+), 6 deletions(-)
-
-diff --git a/dirsrvtests/tests/suites/features/ldap_controls_test.py b/dirsrvtests/tests/suites/features/ldap_controls_test.py
-index 0f8aa08be..59a58b21d 100644
---- a/dirsrvtests/tests/suites/features/ldap_controls_test.py
-+++ b/dirsrvtests/tests/suites/features/ldap_controls_test.py
-@@ -9,15 +9,18 @@
- import logging
- import pytest
- import ldap
-+from ldap.controls import RequestControl
- from ldap.controls.readentry import PostReadControl
- from lib389.idm.user import UserAccounts, UserAccount
--from lib389.topologies import topology_st
--from lib389._constants import DEFAULT_SUFFIX
-+from test389.topologies import topology_st
-+from lib389._constants import DEFAULT_SUFFIX, DN_DM, PASSWORD
-
- pytestmark = pytest.mark.tier1
-
- log = logging.getLogger(__name__)
-
-+MANAGE_DSAIT_OID = "2.16.840.1.113730.3.4.2"
-+MAX_CONTROLS_PER_OP_ATTR = "nsslapd-maxcontrolsperop"
-
- def test_postread_ctrl_modify(topology_st):
- """Test PostReadControl with LDAP modify operations.
-@@ -79,6 +82,75 @@ def test_postread_ctrl_modify(topology_st):
- user.delete()
-
-
-+def _make_request_controls(count):
-+ return [
-+ RequestControl(controlType=MANAGE_DSAIT_OID, criticality=False)
-+ for _ in range(count)
-+ ]
-+
-+
-+def test_bind_excessive_controls(topology_st):
-+ """Bind request control count is limited by nsslapd-maxcontrolsperop
-+
-+ :id: c3888f02-2107-4682-a50a-2189d1436233
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Read nsslapd-maxcontrolsperop from cn=config (default 10)
-+ 2. Bind with one fewer control than the limit
-+ 3. Bind with one more control than the limit
-+ 4. Set nsslapd-maxcontrolsperop to 5
-+ 5. Bind with 4 controls (new limit minus one)
-+ 6. Bind with 6 controls (over new limit)
-+ 7. Restore nsslapd-maxcontrolsperop and re-bind as Directory Manager
-+ :expectedresults:
-+ 1. Config value is 10
-+ 2. Bind succeeds
-+ 3. Bind fails with ldap.UNWILLING_TO_PERFORM
-+ 4. Success
-+ 5. Bind succeeds
-+ 6. Bind fails with ldap.UNWILLING_TO_PERFORM
-+ 7. Success
-+ """
-+ inst = topology_st.standalone
-+ original_max = inst.config.get_attr_val_utf8(MAX_CONTROLS_PER_OP_ATTR)
-+
-+ try:
-+ max_controls = int(inst.config.get_attr_val_utf8(MAX_CONTROLS_PER_OP_ATTR))
-+ assert max_controls == 10
-+
-+ log.info("Bind with %d controls (limit %d, limit minus one)",
-+ max_controls - 1, max_controls)
-+ inst.simple_bind_s(DN_DM, PASSWORD,
-+ serverctrls=_make_request_controls(max_controls - 1))
-+
-+ log.info("Bind with %d controls (limit %d plus one)",
-+ max_controls + 1, max_controls)
-+ with pytest.raises(ldap.UNWILLING_TO_PERFORM):
-+ inst.simple_bind_s(DN_DM, PASSWORD,
-+ serverctrls=_make_request_controls(max_controls + 1))
-+ inst.simple_bind_s(DN_DM, PASSWORD)
-+
-+ lowered_max = 5
-+ log.info("Set %s to %d", MAX_CONTROLS_PER_OP_ATTR, lowered_max)
-+ inst.config.set(MAX_CONTROLS_PER_OP_ATTR, str(lowered_max))
-+ assert int(inst.config.get_attr_val_utf8(MAX_CONTROLS_PER_OP_ATTR)) == lowered_max
-+
-+ log.info("Bind with %d controls (lowered limit %d, limit minus one)",
-+ lowered_max - 1, lowered_max)
-+ inst.simple_bind_s(DN_DM, PASSWORD,
-+ serverctrls=_make_request_controls(lowered_max - 1))
-+
-+ log.info("Bind with %d controls (lowered limit %d plus one)",
-+ lowered_max + 1, lowered_max)
-+ with pytest.raises(ldap.UNWILLING_TO_PERFORM):
-+ inst.simple_bind_s(DN_DM, PASSWORD,
-+ serverctrls=_make_request_controls(lowered_max + 1))
-+ finally:
-+ log.info("Restore %s to %s", MAX_CONTROLS_PER_OP_ATTR, original_max)
-+ inst.config.set(MAX_CONTROLS_PER_OP_ATTR, original_max)
-+ inst.simple_bind_s(DN_DM, PASSWORD)
-+
-+
- if __name__ == '__main__':
- CURRENT_FILE = __file__
- pytest.main(["-s", "-v", CURRENT_FILE])
-diff --git a/ldap/schema/01core389.ldif b/ldap/schema/01core389.ldif
-index bfe8259f8..7e2d1ac44 100644
---- a/ldap/schema/01core389.ldif
-+++ b/ldap/schema/01core389.ldif
-@@ -5,7 +5,7 @@
- # All rights reserved.
- #
- # License: GPL (version 3 or any later version).
--# See LICENSE for details.
-+# See LICENSE for details.
- # END COPYRIGHT BLOCK
- #
- #
-@@ -333,6 +333,7 @@ attributeTypes: ( 2.16.840.1.113730.3.1.2392 NAME 'nsslapd-return-original-entry
- attributeTypes: ( 2.16.840.1.113730.3.1.2393 NAME 'nsslapd-auditlog-display-attrs' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN '389 Directory Server' )
- attributeTypes: ( 2.16.840.1.113730.3.1.2398 NAME 'nsslapd-haproxy-trusted-ip' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN '389 Directory Server' )
- attributeTypes: ( 2.16.840.1.113730.3.1.2400 NAME 'nsslapd-pwdPBKDF2NumIterations' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'Directory Server' )
-+attributeTypes: ( 2.16.840.1.113730.3.1.2402 NAME 'nsslapd-maxcontrolsperop' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN '389 Directory Server' )
- #
- # objectclasses
- #
-diff --git a/ldap/servers/slapd/control.c b/ldap/servers/slapd/control.c
-index d661dc6e1..9373c9f70 100644
---- a/ldap/servers/slapd/control.c
-+++ b/ldap/servers/slapd/control.c
-@@ -302,7 +302,7 @@ get_ldapmessage_controls_ext(
- ber_tag_t tag;
- /* ber_len_t is uint, cannot be -1 */
- ber_len_t len = LBER_ERROR;
-- int rc, maxcontrols, curcontrols;
-+ int rc, maxcontrols, curcontrols, maxcontrols_per_op;
- char *last;
- int managedsait, pwpolicy_ctrl;
- Connection *pb_conn = NULL;
-@@ -379,11 +379,21 @@ get_ldapmessage_controls_ext(
- return (LDAP_PROTOCOL_ERROR);
- }
-
-+ maxcontrols_per_op = config_get_maxcontrolsperop();
- maxcontrols = curcontrols = 0;
- for (tag = ber_first_element(ber, &len, &last);
- tag != LBER_ERROR && tag != LBER_END_OF_SEQORSET;
-- tag = ber_next_element(ber, &len, last)) {
-+ tag = ber_next_element(ber, &len, last))
-+ {
- len = -1; /* reset */
-+ if (curcontrols >= maxcontrols_per_op) {
-+ slapi_log_err(SLAPI_LOG_ERR, "get_ldapmessage_controls_ext",
-+ "Too many controls in LDAP request (max %d)\n",
-+ maxcontrols_per_op);
-+ rc = LDAP_UNWILLING_TO_PERFORM;
-+ goto free_and_return;
-+ }
-+
- if (curcontrols >= maxcontrols - 1) {
- #define CONTROL_GRABSIZE 6
- maxcontrols += CONTROL_GRABSIZE;
-diff --git a/ldap/servers/slapd/libglobs.c b/ldap/servers/slapd/libglobs.c
-index 887ae9a9d..ecf736ef6 100644
---- a/ldap/servers/slapd/libglobs.c
-+++ b/ldap/servers/slapd/libglobs.c
-@@ -1466,6 +1466,11 @@ static struct config_get_and_set
- NULL, 0,
- (void **)&global_slapdFrontendConfig.return_orig_dn,
- CONFIG_ON_OFF, (ConfigGetFunc)config_get_return_orig_dn, &init_return_orig_dn, NULL},
-+ {CONFIG_MAXCONTROLS_PER_OP_ATTRIBUTE, config_set_maxcontrolsperop,
-+ NULL, 0,
-+ (void **)&global_slapdFrontendConfig.maxcontrols_per_op,
-+ CONFIG_INT, (ConfigGetFunc)config_get_maxcontrolsperop,
-+ SLAPD_DEFAULT_MAXCONTROLS_PER_OP_STR, NULL},
- /* End config */
- };
-
-@@ -2041,6 +2046,7 @@ FrontendConfig_init(void)
- init_cn_uses_dn_syntax_in_dns = cfg->cn_uses_dn_syntax_in_dns = LDAP_OFF;
- init_global_backend_local = LDAP_OFF;
- cfg->maxsimplepaged_per_conn = SLAPD_DEFAULT_MAXSIMPLEPAGED_PER_CONN;
-+ cfg->maxcontrols_per_op = SLAPD_DEFAULT_MAXCONTROLS_PER_OP;
- cfg->maxbersize = SLAPD_DEFAULT_MAXBERSIZE;
- cfg->logging_backend = slapi_ch_strdup(SLAPD_INIT_LOGGING_BACKEND_INTERNAL);
- cfg->rootdn = slapi_ch_strdup(SLAPD_DEFAULT_DIRECTORY_MANAGER);
-@@ -10105,6 +10111,52 @@ config_get_maxsimplepaged_per_conn()
- return retVal;
- }
-
-+int
-+config_set_maxcontrolsperop(const char *attrname, char *value, char *errorbuf, int apply)
-+{
-+ int retVal = LDAP_SUCCESS;
-+ slapdFrontendConfig_t *slapdFrontendConfig = getFrontendConfig();
-+ long size;
-+ char *endp;
-+
-+ if (config_value_is_null(attrname, value, errorbuf, 0)) {
-+ return LDAP_OPERATIONS_ERROR;
-+ }
-+
-+ errno = 0;
-+ size = strtol(value, &endp, 10);
-+ if (*endp != '\0' || errno == ERANGE || size < 1 || size > 1000) {
-+ slapi_create_errormsg(errorbuf, SLAPI_DSE_RETURNTEXT_SIZE,
-+ "(%s) value (%s) is invalid, must be at least 1 and less than 1000\n",
-+ attrname, value);
-+ return LDAP_OPERATIONS_ERROR;
-+ }
-+
-+ if (!apply) {
-+ return retVal;
-+ }
-+
-+ CFG_LOCK_WRITE(slapdFrontendConfig);
-+
-+ slapdFrontendConfig->maxcontrols_per_op = size;
-+
-+ CFG_UNLOCK_WRITE(slapdFrontendConfig);
-+ return retVal;
-+}
-+
-+int
-+config_get_maxcontrolsperop()
-+{
-+ slapdFrontendConfig_t *slapdFrontendConfig = getFrontendConfig();
-+ int retVal;
-+
-+ retVal = slapdFrontendConfig->maxcontrols_per_op;
-+ if (retVal == 0) {
-+ retVal = SLAPD_DEFAULT_MAXCONTROLS_PER_OP;
-+ }
-+ return retVal;
-+}
-+
- int32_t
- config_set_extract_pem(const char *attrname, char *value, char *errorbuf, int apply)
- {
-diff --git a/ldap/servers/slapd/proto-slap.h b/ldap/servers/slapd/proto-slap.h
-index 8a2f74836..c6482414f 100644
---- a/ldap/servers/slapd/proto-slap.h
-+++ b/ldap/servers/slapd/proto-slap.h
-@@ -426,6 +426,7 @@ int32_t config_set_maxdescriptors(const char *attrname, char *value, char *error
- int config_set_localuser(const char *attrname, char *value, char *errorbuf, int apply);
-
- int config_set_maxsimplepaged_per_conn(const char *attrname, char *value, char *errorbuf, int apply);
-+int config_set_maxcontrolsperop(const char *attrname, char *value, char *errorbuf, int apply);
-
- int log_external_libs_debug_set_log_fn(void);
- int log_set_backend(const char *attrname, char *value, int logtype, char *errorbuf, int apply);
-@@ -631,6 +632,7 @@ int config_get_malloc_mmap_threshold(void);
- #endif
-
- int config_get_maxsimplepaged_per_conn(void);
-+int config_get_maxcontrolsperop(void);
- int config_get_extract_pem(void);
-
- int32_t config_get_enable_upgrade_hash(void);
-diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h
-index 1e5ad84bf..5b1d8850e 100644
---- a/ldap/servers/slapd/slap.h
-+++ b/ldap/servers/slapd/slap.h
-@@ -301,6 +301,8 @@ typedef void (*VFPV)(); /* takes undefined arguments */
- #define SLAPD_DEFAULT_MAXBERSIZE_STR "2097152"
- #define SLAPD_DEFAULT_MAXSIMPLEPAGED_PER_CONN (-1)
- #define SLAPD_DEFAULT_MAXSIMPLEPAGED_PER_CONN_STR "-1"
-+#define SLAPD_DEFAULT_MAXCONTROLS_PER_OP 10
-+#define SLAPD_DEFAULT_MAXCONTROLS_PER_OP_STR "10"
- #define SLAPD_DEFAULT_LDAPSSOTOKEN_TTL 3600
- #define SLAPD_DEFAULT_LDAPSSOTOKEN_TTL_STR "3600"
-
-@@ -2417,6 +2419,7 @@ typedef struct _slapdEntryPoints
- #define CONFIG_CN_USES_DN_SYNTAX_IN_DNS "nsslapd-cn-uses-dn-syntax-in-dns"
-
- #define CONFIG_MAXSIMPLEPAGED_PER_CONN_ATTRIBUTE "nsslapd-maxsimplepaged-per-conn"
-+#define CONFIG_MAXCONTROLS_PER_OP_ATTRIBUTE "nsslapd-maxcontrolsperop"
- #define CONFIG_LOGGING_BACKEND "nsslapd-logging-backend"
-
- #define CONFIG_EXTRACT_PEM "nsslapd-extract-pemfiles"
-@@ -2749,6 +2752,7 @@ typedef struct _slapdFrontendConfig
- slapi_onoff_t cn_uses_dn_syntax_in_dns; /* indicates the cn value in dns has dn syntax */
- slapi_onoff_t global_backend_lock;
- slapi_int_t maxsimplepaged_per_conn; /* max simple paged results reqs handled per connection */
-+ slapi_int_t maxcontrols_per_op; /* max LDAP controls allowed per operation */
- slapi_onoff_t enable_nunc_stans; /* Despite the removal of NS, we have to leave the value in
- * case someone was setting it.
- */
-diff --git a/src/cockpit/389-console/src/lib/server/tuning.jsx b/src/cockpit/389-console/src/lib/server/tuning.jsx
-index 5f56ff858..fe0ed9d2e 100644
---- a/src/cockpit/389-console/src/lib/server/tuning.jsx
-+++ b/src/cockpit/389-console/src/lib/server/tuning.jsx
-@@ -23,6 +23,7 @@ const tuning_attrs = [
- 'nsslapd-connection-nocanon',
- 'nsslapd-enable-turbo-mode',
- 'nsslapd-threadnumber',
-+ 'nsslapd-maxthreadsperconn',
- 'nsslapd-maxdescriptors',
- 'nsslapd-timelimit',
- 'nsslapd-sizelimit',
-@@ -34,6 +35,7 @@ const tuning_attrs = [
- 'nsslapd-maxsasliosize',
- 'nsslapd-listen-backlog-size',
- 'nsslapd-max-filter-nest-level',
-+ 'nsslapd-maxcontrolsperop',
- 'nsslapd-ndn-cache-max-size',
- ];
-
-@@ -161,6 +163,7 @@ export class ServerTuning extends React.Component {
- 'nsslapd-connection-nocanon': connNoCannon,
- 'nsslapd-enable-turbo-mode': turboMode,
- 'nsslapd-threadnumber': attrs['nsslapd-threadnumber'][0],
-+ 'nsslapd-maxthreadsperconn': attrs['nsslapd-maxthreadsperconn'][0],
- 'nsslapd-maxdescriptors': attrs['nsslapd-maxdescriptors'][0],
- 'nsslapd-timelimit': attrs['nsslapd-timelimit'][0],
- 'nsslapd-sizelimit': attrs['nsslapd-sizelimit'][0],
-@@ -172,6 +175,7 @@ export class ServerTuning extends React.Component {
- 'nsslapd-maxsasliosize': attrs['nsslapd-maxsasliosize'][0],
- 'nsslapd-listen-backlog-size': attrs['nsslapd-listen-backlog-size'][0],
- 'nsslapd-max-filter-nest-level': attrs['nsslapd-max-filter-nest-level'][0],
-+ 'nsslapd-maxcontrolsperop': attrs['nsslapd-maxcontrolsperop'][0],
- 'nsslapd-ndn-cache-max-size': attrs['nsslapd-ndn-cache-max-size'][0],
- // Record original values
- '_nsslapd-ndn-cache-enabled': ndnEnabled,
-@@ -179,6 +183,7 @@ export class ServerTuning extends React.Component {
- '_nsslapd-connection-nocanon': connNoCannon,
- '_nsslapd-enable-turbo-mode': turboMode,
- '_nsslapd-threadnumber': attrs['nsslapd-threadnumber'][0],
-+ '_nsslapd-maxthreadsperconn': attrs['nsslapd-maxthreadsperconn'][0],
- '_nsslapd-maxdescriptors': attrs['nsslapd-maxdescriptors'][0],
- '_nsslapd-timelimit': attrs['nsslapd-timelimit'][0],
- '_nsslapd-sizelimit': attrs['nsslapd-sizelimit'][0],
-@@ -190,6 +195,7 @@ export class ServerTuning extends React.Component {
- '_nsslapd-maxsasliosize': attrs['nsslapd-maxsasliosize'][0],
- '_nsslapd-listen-backlog-size': attrs['nsslapd-listen-backlog-size'][0],
- '_nsslapd-max-filter-nest-level': attrs['nsslapd-max-filter-nest-level'][0],
-+ '_nsslapd-maxcontrolsperop': attrs['nsslapd-maxcontrolsperop'][0],
- '_nsslapd-ndn-cache-max-size': attrs['nsslapd-ndn-cache-max-size'][0],
- }, this.props.enableTree());
- })
-@@ -275,7 +281,7 @@ export class ServerTuning extends React.Component {
- <TextContent>
- <Text component={TextVariants.h3}>
- {_("Tuning & Limits")}
-- <Button
-+ <Button
- variant="plain"
- aria-label={_("Refresh settings")}
- onClick={() => {
-@@ -312,6 +318,28 @@ export class ServerTuning extends React.Component {
- />
- </GridItem>
- </Grid>
-+ <Grid
-+ title={_("The maximum number of threads that can handle requests for a single connection (nsslapd-maxthreadsperconn).")}
-+ >
-+ <GridItem className="ds-label" span={3}>
-+ {_("Max Threads Per Connection")}
-+ </GridItem>
-+ <GridItem span={9}>
-+ <NumberInput
-+ value={this.state['nsslapd-maxthreadsperconn']}
-+ min={1}
-+ max={65535}
-+ onMinus={() => { this.onMinusConfig("nsslapd-maxthreadsperconn") }}
-+ onChange={(e) => { this.onConfigChange(e, "nsslapd-maxthreadsperconn", 1, 65535) }}
-+ onPlus={() => { this.onPlusConfig("nsslapd-maxthreadsperconn") }}
-+ inputName="input"
-+ inputAriaLabel="number input"
-+ minusBtnAriaLabel="minus"
-+ plusBtnAriaLabel="plus"
-+ widthChars={8}
-+ />
-+ </GridItem>
-+ </Grid>
- <Grid
- title={_("The maximum number of seconds allocated for a search request. Set to '-1' to disable the time limit (nsslapd-timelimit).")}
- >
-@@ -542,6 +570,28 @@ export class ServerTuning extends React.Component {
- />
- </GridItem>
- </Grid>
-+ <Grid
-+ title={_("The maximum number of LDAP controls allowed per operation (nsslapd-maxcontrolsperop).")}
-+ >
-+ <GridItem className="ds-label" span={3}>
-+ {_("Maximum Controls Per Operation")}
-+ </GridItem>
-+ <GridItem span={9}>
-+ <NumberInput
-+ value={this.state['nsslapd-maxcontrolsperop']}
-+ min={1}
-+ max={1000}
-+ onMinus={() => { this.onMinusConfig("nsslapd-maxcontrolsperop") }}
-+ onChange={(e) => { this.onConfigChange(e, "nsslapd-maxcontrolsperop", 1, 0) }}
-+ onPlus={() => { this.onPlusConfig("nsslapd-maxcontrolsperop") }}
-+ inputName="input"
-+ inputAriaLabel="number input"
-+ minusBtnAriaLabel="minus"
-+ plusBtnAriaLabel="plus"
-+ widthChars={8}
-+ />
-+ </GridItem>
-+ </Grid>
- <Grid
- title={_("Disable DNS reverse entries for outgoing connections (nsslapd-connection-nocanon).")}
- >
---
-2.54.0
-
diff --git a/389-ds-base.spec b/389-ds-base.spec
index 965c8fc..8059312 100644
--- a/389-ds-base.spec
+++ b/389-ds-base.spec
@@ -1,6 +1,6 @@
%global pkgname dirsrv
-# Exclude i686 bit arches
+# Exclude i686 architecture
ExcludeArch: i686
%bcond bundle_jemalloc 1
@@ -10,11 +10,7 @@ ExcludeArch: i686
%global __provides_exclude ^libjemalloc\\.so.*$
%endif
-%bcond bundle_libdb 0
-%if 0%{?rhel} >= 10
-%bcond bundle_libdb 1
-%endif
-
+%bcond bundle_libdb %{defined rhel}
%if %{with bundle_libdb}
%global libdb_version 5.3
%global libdb_base_version db-%{libdb_version}.28
@@ -28,13 +24,18 @@ ExcludeArch: i686
%endif
%endif
-%bcond libbdb_ro 0
+%bcond repl_reports 0
+
%if 0%{?fedora} >= 43
%bcond libbdb_ro 1
+%else
+%bcond libbdb_ro 0
%endif
# This is used in certain builds to help us know if it has extra features.
%global variant base
+%global prerel %{nil}
+
# This enables a sanitized build.
%bcond asan 0
%bcond msan 0
@@ -53,21 +54,30 @@ ExcludeArch: i686
%if %{with clang}
%global toolchain clang
-%global _missing_build_ids_terminate_build 0
+%global _lto_cflags %nil
%endif
# Build cockpit plugin
+# Enabled on Fedora and EPEL community builds.
+# Disabled on RHEL/CentOS Stream unless RHDS (distribution contains "dsrv").
+%if 0%{?rhel} && !0%{?epel}
+%bcond cockpit %(echo "%{?distribution}" | grep -q dsrv && echo 1 || echo 0)
+%else
%bcond cockpit 1
+%endif
-# fedora 15 and later uses tmpfiles.d
-# otherwise, comment this out
-%{!?with_tmpfiles_d: %global with_tmpfiles_d %{_sysconfdir}/tmpfiles.d}
+# HIBP password breach checking
+%bcond hibp 1
+
+%bcond usdt 1
# systemd support
%global groupname %{pkgname}.target
# Filter argparse-manpage from autogenerated package Requires
%global __requires_exclude ^python.*argparse-manpage
+# Filter perl auto-dep from jemalloc's jeprof profiler script
+%global __requires_exclude_from %{_libdir}/%{pkgname}/bin/jeprof
# Force to require nss version greater or equal as the version available at the build time
# See bz1986327
@@ -75,24 +85,21 @@ ExcludeArch: i686
Summary: 389 Directory Server (%{variant})
Name: 389-ds-base
-Version: 3.1.4
+Version: 3.1.5%{?prerel}
Release: %{autorelease -n %{?with_asan:-e asan}}%{?dist}
-License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT) AND (CC-BY-4.0 AND MIT) AND (MIT OR Apache-2.0) AND Unicode-3.0 AND (MIT OR CC0-1.0) AND (MIT OR Unlicense) AND 0BSD AND Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND ISC AND MIT AND MIT AND ISC AND MPL-2.0 AND PSF-2.0 AND Zlib
+License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT) AND (Apache-2.0 OR MIT) AND Unicode-3.0 AND (CC-BY-4.0 AND MIT) AND (MIT OR Unlicense) AND 0BSD AND Apache-2.0 AND BSD-3-Clause AND ISC AND MIT AND MIT AND ISC AND MPL-2.0 AND Zlib
URL: https://www.port389.org
-Obsoletes: %{name}-legacy-tools < 1.4.4.6
-Obsoletes: %{name}-legacy-tools-debuginfo < 1.4.4.6
-Provides: ldif2ldbm >= 0
##### Bundled cargo crates list - START #####
Provides: bundled(crate(allocator-api2)) = 0.2.21
Provides: bundled(crate(anyhow)) = 1.0.102
Provides: bundled(crate(atty)) = 0.2.14
-Provides: bundled(crate(autocfg)) = 1.5.1
-Provides: bundled(crate(base64)) = 0.13.1
-Provides: bundled(crate(bitflags)) = 2.12.1
+Provides: bundled(crate(autocfg)) = 1.5.0
+Provides: bundled(crate(base64)) = 0.23.0
+Provides: bundled(crate(bitflags)) = 2.11.0
Provides: bundled(crate(byteorder)) = 1.5.0
Provides: bundled(crate(cbindgen)) = 0.26.0
-Provides: bundled(crate(cc)) = 1.2.63
+Provides: bundled(crate(cc)) = 1.2.56
Provides: bundled(crate(cfg-if)) = 1.0.4
Provides: bundled(crate(clap)) = 3.2.25
Provides: bundled(crate(clap_lex)) = 0.2.4
@@ -102,55 +109,55 @@ Provides: bundled(crate(crossbeam-queue)) = 0.3.12
Provides: bundled(crate(crossbeam-utils)) = 0.8.21
Provides: bundled(crate(equivalent)) = 1.0.2
Provides: bundled(crate(errno)) = 0.3.14
-Provides: bundled(crate(fastrand)) = 2.4.1
+Provides: bundled(crate(fastrand)) = 2.3.0
Provides: bundled(crate(fernet)) = 0.1.4
Provides: bundled(crate(find-msvc-tools)) = 0.1.9
Provides: bundled(crate(foldhash)) = 0.2.0
Provides: bundled(crate(foreign-types)) = 0.3.2
Provides: bundled(crate(foreign-types-shared)) = 0.1.1
-Provides: bundled(crate(getrandom)) = 0.4.2
-Provides: bundled(crate(hashbrown)) = 0.17.1
+Provides: bundled(crate(getrandom)) = 0.4.1
+Provides: bundled(crate(hashbrown)) = 0.16.1
Provides: bundled(crate(heck)) = 0.5.0
Provides: bundled(crate(hermit-abi)) = 0.1.19
Provides: bundled(crate(id-arena)) = 2.3.0
-Provides: bundled(crate(indexmap)) = 2.14.0
-Provides: bundled(crate(itoa)) = 1.0.18
+Provides: bundled(crate(indexmap)) = 2.13.0
+Provides: bundled(crate(itoa)) = 1.0.17
Provides: bundled(crate(jobserver)) = 0.1.34
Provides: bundled(crate(leb128fmt)) = 0.1.0
-Provides: bundled(crate(libc)) = 0.2.186
-Provides: bundled(crate(linux-raw-sys)) = 0.12.1
-Provides: bundled(crate(log)) = 0.4.31
-Provides: bundled(crate(lru)) = 0.16.4
-Provides: bundled(crate(memchr)) = 2.8.1
-Provides: bundled(crate(once_cell)) = 1.21.4
-Provides: bundled(crate(openssl)) = 0.10.80
+Provides: bundled(crate(libc)) = 0.2.182
+Provides: bundled(crate(linux-raw-sys)) = 0.11.0
+Provides: bundled(crate(log)) = 0.4.29
+Provides: bundled(crate(lru)) = 0.16.3
+Provides: bundled(crate(memchr)) = 2.8.0
+Provides: bundled(crate(once_cell)) = 1.21.3
+Provides: bundled(crate(openssl)) = 0.10.75
Provides: bundled(crate(openssl-macros)) = 0.1.1
-Provides: bundled(crate(openssl-sys)) = 0.9.116
+Provides: bundled(crate(openssl-sys)) = 0.9.111
Provides: bundled(crate(os_str_bytes)) = 6.6.1
Provides: bundled(crate(paste)) = 0.1.18
Provides: bundled(crate(paste-impl)) = 0.1.18
-Provides: bundled(crate(pin-project-lite)) = 0.2.17
-Provides: bundled(crate(pkg-config)) = 0.3.33
+Provides: bundled(crate(pin-project-lite)) = 0.2.16
+Provides: bundled(crate(pkg-config)) = 0.3.32
Provides: bundled(crate(prettyplease)) = 0.2.37
Provides: bundled(crate(proc-macro-hack)) = 0.5.20+deprecated
Provides: bundled(crate(proc-macro2)) = 1.0.106
-Provides: bundled(crate(quote)) = 1.0.45
-Provides: bundled(crate(r-efi)) = 6.0.0
-Provides: bundled(crate(rustix)) = 1.1.4
-Provides: bundled(crate(semver)) = 1.0.28
+Provides: bundled(crate(quote)) = 1.0.44
+Provides: bundled(crate(r-efi)) = 5.3.0
+Provides: bundled(crate(rustix)) = 1.1.3
+Provides: bundled(crate(semver)) = 1.0.27
Provides: bundled(crate(serde)) = 1.0.228
Provides: bundled(crate(serde_core)) = 1.0.228
Provides: bundled(crate(serde_derive)) = 1.0.228
-Provides: bundled(crate(serde_json)) = 1.0.150
-Provides: bundled(crate(shlex)) = 2.0.1
+Provides: bundled(crate(serde_json)) = 1.0.149
+Provides: bundled(crate(shlex)) = 1.3.0
Provides: bundled(crate(smallvec)) = 1.15.1
Provides: bundled(crate(sptr)) = 0.3.2
Provides: bundled(crate(strsim)) = 0.10.0
Provides: bundled(crate(syn)) = 2.0.117
-Provides: bundled(crate(tempfile)) = 3.27.0
+Provides: bundled(crate(tempfile)) = 3.25.0
Provides: bundled(crate(termcolor)) = 1.4.1
Provides: bundled(crate(textwrap)) = 0.16.2
-Provides: bundled(crate(tokio)) = 1.52.3
+Provides: bundled(crate(tokio)) = 1.49.0
Provides: bundled(crate(toml)) = 0.5.11
Provides: bundled(crate(tracing)) = 0.1.44
Provides: bundled(crate(tracing-attributes)) = 0.1.31
@@ -160,7 +167,7 @@ Provides: bundled(crate(unicode-xid)) = 0.2.6
Provides: bundled(crate(uuid)) = 0.8.2
Provides: bundled(crate(vcpkg)) = 0.2.15
Provides: bundled(crate(wasi)) = 0.11.1+wasi_snapshot_preview1
-Provides: bundled(crate(wasip2)) = 1.0.3+wasi_0.2.9
+Provides: bundled(crate(wasip2)) = 1.0.2+wasi_0.2.9
Provides: bundled(crate(wasip3)) = 0.4.0+wasi_0.3.0_rc_2026_01_06
Provides: bundled(crate(wasm-encoder)) = 0.244.0
Provides: bundled(crate(wasm-metadata)) = 0.244.0
@@ -171,7 +178,7 @@ Provides: bundled(crate(winapi-util)) = 0.1.11
Provides: bundled(crate(winapi-x86_64-pc-windows-gnu)) = 0.4.0
Provides: bundled(crate(windows-link)) = 0.2.1
Provides: bundled(crate(windows-sys)) = 0.61.2
-Provides: bundled(crate(wit-bindgen)) = 0.57.1
+Provides: bundled(crate(wit-bindgen)) = 0.51.0
Provides: bundled(crate(wit-bindgen-core)) = 0.51.0
Provides: bundled(crate(wit-bindgen-rust)) = 0.51.0
Provides: bundled(crate(wit-bindgen-rust-macro)) = 0.51.0
@@ -180,20 +187,10 @@ Provides: bundled(crate(wit-parser)) = 0.244.0
Provides: bundled(crate(zeroize)) = 1.8.2
Provides: bundled(crate(zeroize_derive)) = 1.4.3
Provides: bundled(crate(zmij)) = 1.0.21
-Provides: bundled(npm(@eslint-community/eslint-utils)) = 4.4.1
-Provides: bundled(npm(@eslint-community/regexpp)) = 4.12.1
-Provides: bundled(npm(@eslint/eslintrc)) = 2.1.4
-Provides: bundled(npm(@eslint/js)) = 8.57.1
Provides: bundled(npm(@fortawesome/fontawesome-common-types)) = 0.2.36
Provides: bundled(npm(@fortawesome/fontawesome-svg-core)) = 1.2.36
Provides: bundled(npm(@fortawesome/free-solid-svg-icons)) = 5.15.4
Provides: bundled(npm(@fortawesome/react-fontawesome)) = 0.1.19
-Provides: bundled(npm(@humanwhocodes/config-array)) = 0.13.0
-Provides: bundled(npm(@humanwhocodes/module-importer)) = 1.0.1
-Provides: bundled(npm(@humanwhocodes/object-schema)) = 2.0.3
-Provides: bundled(npm(@nodelib/fs.scandir)) = 2.1.5
-Provides: bundled(npm(@nodelib/fs.stat)) = 2.0.5
-Provides: bundled(npm(@nodelib/fs.walk)) = 1.2.8
Provides: bundled(npm(@patternfly/patternfly)) = 5.4.1
Provides: bundled(npm(@patternfly/react-charts)) = 7.4.3
Provides: bundled(npm(@patternfly/react-core)) = 5.4.1
@@ -202,39 +199,25 @@ Provides: bundled(npm(@patternfly/react-log-viewer)) = 5.3.0
Provides: bundled(npm(@patternfly/react-styles)) = 5.4.0
Provides: bundled(npm(@patternfly/react-table)) = 5.4.1
Provides: bundled(npm(@patternfly/react-tokens)) = 5.4.0
-Provides: bundled(npm(@types/d3-array)) = 3.2.1
+Provides: bundled(npm(@types/d3-array)) = 3.2.2
Provides: bundled(npm(@types/d3-color)) = 3.1.3
Provides: bundled(npm(@types/d3-ease)) = 3.0.2
Provides: bundled(npm(@types/d3-interpolate)) = 3.0.4
-Provides: bundled(npm(@types/d3-path)) = 3.1.0
-Provides: bundled(npm(@types/d3-scale)) = 4.0.8
-Provides: bundled(npm(@types/d3-shape)) = 3.1.6
-Provides: bundled(npm(@types/d3-time)) = 3.0.3
+Provides: bundled(npm(@types/d3-path)) = 3.1.1
+Provides: bundled(npm(@types/d3-scale)) = 4.0.9
+Provides: bundled(npm(@types/d3-shape)) = 3.1.8
+Provides: bundled(npm(@types/d3-time)) = 3.0.4
Provides: bundled(npm(@types/d3-timer)) = 3.0.2
-Provides: bundled(npm(@ungap/structured-clone)) = 1.2.0
Provides: bundled(npm(@xterm/addon-canvas)) = 0.7.0
Provides: bundled(npm(@xterm/xterm)) = 5.5.0
-Provides: bundled(npm(acorn)) = 8.14.0
-Provides: bundled(npm(acorn-jsx)) = 5.3.2
-Provides: bundled(npm(ajv)) = 6.15.0
-Provides: bundled(npm(ansi-regex)) = 5.0.1
-Provides: bundled(npm(ansi-styles)) = 4.3.0
-Provides: bundled(npm(argparse)) = 2.0.1
-Provides: bundled(npm(attr-accept)) = 2.2.4
+Provides: bundled(npm(argparse)) = 1.0.10
+Provides: bundled(npm(attr-accept)) = 2.2.5
Provides: bundled(npm(autolinker)) = 3.16.2
-Provides: bundled(npm(balanced-match)) = 1.0.2
-Provides: bundled(npm(brace-expansion)) = 1.1.15
-Provides: bundled(npm(callsites)) = 3.1.0
-Provides: bundled(npm(chalk)) = 4.1.2
-Provides: bundled(npm(color-convert)) = 2.0.1
-Provides: bundled(npm(color-name)) = 1.1.4
-Provides: bundled(npm(concat-map)) = 0.0.1
Provides: bundled(npm(core-util-is)) = 1.0.3
-Provides: bundled(npm(cross-spawn)) = 7.0.6
Provides: bundled(npm(d3-array)) = 3.2.4
Provides: bundled(npm(d3-color)) = 3.1.0
Provides: bundled(npm(d3-ease)) = 3.0.1
-Provides: bundled(npm(d3-format)) = 3.1.0
+Provides: bundled(npm(d3-format)) = 3.1.2
Provides: bundled(npm(d3-interpolate)) = 3.0.1
Provides: bundled(npm(d3-path)) = 3.1.0
Provides: bundled(npm(d3-scale)) = 4.0.2
@@ -242,145 +225,72 @@ Provides: bundled(npm(d3-shape)) = 3.2.0
Provides: bundled(npm(d3-time)) = 3.1.0
Provides: bundled(npm(d3-time-format)) = 4.1.0
Provides: bundled(npm(d3-timer)) = 3.0.1
-Provides: bundled(npm(debug)) = 4.3.7
-Provides: bundled(npm(deep-is)) = 0.1.4
Provides: bundled(npm(delaunator)) = 4.0.1
Provides: bundled(npm(delaunay-find)) = 0.0.6
Provides: bundled(npm(dequal)) = 2.0.3
-Provides: bundled(npm(doctrine)) = 3.0.0
Provides: bundled(npm(encoding)) = 0.1.13
-Provides: bundled(npm(escape-string-regexp)) = 4.0.0
-Provides: bundled(npm(eslint)) = 8.57.1
-Provides: bundled(npm(eslint-plugin-react-hooks)) = 4.6.2
-Provides: bundled(npm(eslint-scope)) = 7.2.2
-Provides: bundled(npm(eslint-visitor-keys)) = 3.4.3
-Provides: bundled(npm(espree)) = 9.6.1
-Provides: bundled(npm(esquery)) = 1.6.0
-Provides: bundled(npm(esrecurse)) = 4.3.0
-Provides: bundled(npm(estraverse)) = 5.3.0
-Provides: bundled(npm(esutils)) = 2.0.3
-Provides: bundled(npm(fast-deep-equal)) = 3.1.3
-Provides: bundled(npm(fast-json-stable-stringify)) = 2.1.0
-Provides: bundled(npm(fast-levenshtein)) = 2.0.6
-Provides: bundled(npm(fastq)) = 1.17.1
-Provides: bundled(npm(file-entry-cache)) = 6.0.1
-Provides: bundled(npm(file-selector)) = 2.1.0
-Provides: bundled(npm(find-up)) = 5.0.0
-Provides: bundled(npm(flat-cache)) = 3.2.0
-Provides: bundled(npm(flatted)) = 3.4.2
+Provides: bundled(npm(file-selector)) = 2.1.2
Provides: bundled(npm(focus-trap)) = 7.5.4
-Provides: bundled(npm(fs.realpath)) = 1.0.0
Provides: bundled(npm(gettext-parser)) = 2.1.0
-Provides: bundled(npm(glob)) = 7.2.3
-Provides: bundled(npm(glob-parent)) = 6.0.2
-Provides: bundled(npm(globals)) = 13.24.0
-Provides: bundled(npm(graphemer)) = 1.4.0
-Provides: bundled(npm(has-flag)) = 4.0.0
Provides: bundled(npm(hoist-non-react-statics)) = 3.3.2
Provides: bundled(npm(iconv-lite)) = 0.6.3
-Provides: bundled(npm(ignore)) = 5.3.2
-Provides: bundled(npm(import-fresh)) = 3.3.0
-Provides: bundled(npm(imurmurhash)) = 0.1.4
-Provides: bundled(npm(inflight)) = 1.0.6
Provides: bundled(npm(inherits)) = 2.0.4
Provides: bundled(npm(internmap)) = 2.0.3
-Provides: bundled(npm(is-extglob)) = 2.1.1
-Provides: bundled(npm(is-glob)) = 4.0.3
-Provides: bundled(npm(is-path-inside)) = 3.0.3
Provides: bundled(npm(isarray)) = 1.0.0
-Provides: bundled(npm(isexe)) = 2.0.0
Provides: bundled(npm(js-sha1)) = 0.7.0
Provides: bundled(npm(js-sha256)) = 0.11.0
Provides: bundled(npm(js-tokens)) = 4.0.0
-Provides: bundled(npm(js-yaml)) = 4.1.1
-Provides: bundled(npm(json-buffer)) = 3.0.1
-Provides: bundled(npm(json-schema-traverse)) = 0.4.1
Provides: bundled(npm(json-stable-stringify-without-jsonify)) = 1.0.1
Provides: bundled(npm(json-stringify-safe)) = 5.0.1
-Provides: bundled(npm(keyv)) = 4.5.4
-Provides: bundled(npm(levn)) = 0.4.1
-Provides: bundled(npm(locate-path)) = 6.0.0
Provides: bundled(npm(lodash)) = 4.18.1
-Provides: bundled(npm(lodash.merge)) = 4.6.2
Provides: bundled(npm(loose-envify)) = 1.4.0
Provides: bundled(npm(memoize-one)) = 5.2.1
-Provides: bundled(npm(minimatch)) = 3.1.5
-Provides: bundled(npm(ms)) = 2.1.3
-Provides: bundled(npm(natural-compare)) = 1.4.0
Provides: bundled(npm(object-assign)) = 4.1.1
-Provides: bundled(npm(once)) = 1.4.0
-Provides: bundled(npm(optionator)) = 0.9.4
-Provides: bundled(npm(p-limit)) = 3.1.0
-Provides: bundled(npm(p-locate)) = 5.0.0
-Provides: bundled(npm(parent-module)) = 1.0.1
-Provides: bundled(npm(path-exists)) = 4.0.0
-Provides: bundled(npm(path-is-absolute)) = 1.0.1
-Provides: bundled(npm(path-key)) = 3.1.1
-Provides: bundled(npm(prelude-ls)) = 1.2.1
-Provides: bundled(npm(prettier)) = 3.3.3
+Provides: bundled(npm(prettier)) = 3.8.3
Provides: bundled(npm(process-nextick-args)) = 2.0.1
Provides: bundled(npm(prop-types)) = 15.8.1
-Provides: bundled(npm(punycode)) = 2.3.1
-Provides: bundled(npm(queue-microtask)) = 1.2.3
Provides: bundled(npm(react)) = 18.3.1
Provides: bundled(npm(react-dom)) = 18.3.1
-Provides: bundled(npm(react-dropzone)) = 14.3.5
+Provides: bundled(npm(react-dropzone)) = 14.4.1
Provides: bundled(npm(react-fast-compare)) = 3.2.2
Provides: bundled(npm(react-is)) = 16.13.1
Provides: bundled(npm(readable-stream)) = 2.3.8
Provides: bundled(npm(remarkable)) = 2.0.1
-Provides: bundled(npm(resolve-from)) = 4.0.0
-Provides: bundled(npm(reusify)) = 1.0.4
-Provides: bundled(npm(rimraf)) = 3.0.2
-Provides: bundled(npm(run-parallel)) = 1.2.0
Provides: bundled(npm(safe-buffer)) = 5.2.1
Provides: bundled(npm(safer-buffer)) = 2.1.2
Provides: bundled(npm(scheduler)) = 0.23.2
-Provides: bundled(npm(shebang-command)) = 2.0.0
-Provides: bundled(npm(shebang-regex)) = 3.0.0
Provides: bundled(npm(sprintf-js)) = 1.0.3
Provides: bundled(npm(string_decoder)) = 1.1.1
-Provides: bundled(npm(strip-ansi)) = 6.0.1
-Provides: bundled(npm(strip-json-comments)) = 3.1.1
-Provides: bundled(npm(supports-color)) = 7.2.0
-Provides: bundled(npm(tabbable)) = 6.2.0
-Provides: bundled(npm(text-table)) = 0.2.0
+Provides: bundled(npm(tabbable)) = 6.4.0
Provides: bundled(npm(throttle-debounce)) = 5.0.2
Provides: bundled(npm(tslib)) = 2.8.1
-Provides: bundled(npm(type-check)) = 0.4.0
-Provides: bundled(npm(type-fest)) = 0.20.2
-Provides: bundled(npm(uri-js)) = 4.4.1
Provides: bundled(npm(util-deprecate)) = 1.0.2
-Provides: bundled(npm(uuid)) = 10.0.0
-Provides: bundled(npm(victory-area)) = 37.3.1
-Provides: bundled(npm(victory-axis)) = 37.3.1
-Provides: bundled(npm(victory-bar)) = 37.3.1
-Provides: bundled(npm(victory-box-plot)) = 37.3.1
-Provides: bundled(npm(victory-brush-container)) = 37.3.1
-Provides: bundled(npm(victory-chart)) = 37.3.1
-Provides: bundled(npm(victory-core)) = 37.3.1
-Provides: bundled(npm(victory-create-container)) = 37.3.1
-Provides: bundled(npm(victory-cursor-container)) = 37.3.1
-Provides: bundled(npm(victory-group)) = 37.3.1
-Provides: bundled(npm(victory-legend)) = 37.3.1
-Provides: bundled(npm(victory-line)) = 37.3.1
-Provides: bundled(npm(victory-pie)) = 37.3.1
-Provides: bundled(npm(victory-polar-axis)) = 37.3.1
-Provides: bundled(npm(victory-scatter)) = 37.3.1
-Provides: bundled(npm(victory-selection-container)) = 37.3.1
-Provides: bundled(npm(victory-shared-events)) = 37.3.1
-Provides: bundled(npm(victory-stack)) = 37.3.1
-Provides: bundled(npm(victory-tooltip)) = 37.3.1
-Provides: bundled(npm(victory-vendor)) = 37.3.1
-Provides: bundled(npm(victory-voronoi-container)) = 37.3.1
-Provides: bundled(npm(victory-zoom-container)) = 37.3.1
-Provides: bundled(npm(which)) = 2.0.2
-Provides: bundled(npm(word-wrap)) = 1.2.5
-Provides: bundled(npm(wrappy)) = 1.0.2
-Provides: bundled(npm(yocto-queue)) = 0.1.0
+Provides: bundled(npm(uuid)) = 14.0.0
+Provides: bundled(npm(victory-area)) = 37.3.6
+Provides: bundled(npm(victory-axis)) = 37.3.6
+Provides: bundled(npm(victory-bar)) = 37.3.6
+Provides: bundled(npm(victory-box-plot)) = 37.3.6
+Provides: bundled(npm(victory-brush-container)) = 37.3.6
+Provides: bundled(npm(victory-chart)) = 37.3.6
+Provides: bundled(npm(victory-core)) = 37.3.6
+Provides: bundled(npm(victory-create-container)) = 37.3.6
+Provides: bundled(npm(victory-cursor-container)) = 37.3.6
+Provides: bundled(npm(victory-group)) = 37.3.6
+Provides: bundled(npm(victory-legend)) = 37.3.6
+Provides: bundled(npm(victory-line)) = 37.3.6
+Provides: bundled(npm(victory-pie)) = 37.3.6
+Provides: bundled(npm(victory-polar-axis)) = 37.3.6
+Provides: bundled(npm(victory-scatter)) = 37.3.6
+Provides: bundled(npm(victory-selection-container)) = 37.3.6
+Provides: bundled(npm(victory-shared-events)) = 37.3.6
+Provides: bundled(npm(victory-stack)) = 37.3.6
+Provides: bundled(npm(victory-tooltip)) = 37.3.6
+Provides: bundled(npm(victory-vendor)) = 37.3.6
+Provides: bundled(npm(victory-voronoi-container)) = 37.3.6
+Provides: bundled(npm(victory-zoom-container)) = 37.3.6
##### Bundled cargo crates list - END #####
-# Attach the buildrequires to the top level package:
+# BuildRequires for the main package:
BuildRequires: nspr-devel
BuildRequires: nss-devel >= 3.34
BuildRequires: openldap-clients
@@ -393,6 +303,7 @@ BuildRequires: pcre2-devel
BuildRequires: cracklib-devel
BuildRequires: json-c-devel
BuildRequires: libxcrypt-devel
+BuildRequires: zlib-devel
%if %{with clang}
BuildRequires: libatomic
BuildRequires: clang
@@ -417,63 +328,78 @@ BuildRequires: libdb-devel
%endif
%endif
-# The following are needed to build the snmp ldap-agent
+# For SNMP ldap-agent
BuildRequires: net-snmp-devel
BuildRequires: bzip2-devel
BuildRequires: openssl-devel
-# the following is for the pam passthru auth plug-in
+# For HIBP password breach checking
+%if %{with hibp}
+BuildRequires: libcurl-devel
+%endif
+# For PAM passthru auth plug-in
BuildRequires: pam-devel
BuildRequires: systemd-units
BuildRequires: systemd-devel
BuildRequires: systemd-rpm-macros
%{?sysusers_requires_compat}
+%if %{with usdt}
+BuildRequires: systemtap-sdt-devel
+%endif
BuildRequires: cargo
BuildRequires: rust
BuildRequires: pkgconfig
BuildRequires: pkgconfig(systemd)
BuildRequires: pkgconfig(krb5)
BuildRequires: pkgconfig(libpcre2-8)
-# Needed to support regeneration of the autotool artifacts.
+# For autotools regeneration
BuildRequires: autoconf
BuildRequires: automake
BuildRequires: libtool
-# For our documentation
+BuildRequires: make
+# For doxygen documentation
BuildRequires: doxygen
-# For tests!
+# For cmocka unit tests
BuildRequires: libcmocka-devel
-# For lib389 and related components.
+# For lib389
BuildRequires: python%{python3_pkgversion}-devel
# For cockpit
%if %{with cockpit}
BuildRequires: rsync
-BuildRequires: npm
-BuildRequires: nodejs
+BuildRequires: /usr/bin/npm
+BuildRequires: /usr/bin/node
%endif
-# For autosetup -S git
-BuildRequires: git
-
+# Runtime requires for the main package
Requires: %{name}-libs = %{version}-%{release}
Requires: python%{python3_pkgversion}-lib389 = %{version}-%{release}
-# this is needed for using semanage from our setup scripts
+# For semanage in setup scripts
Requires: policycoreutils-python-utils
Requires: libsemanage-python%{python3_pkgversion}
-# the following are needed for some of our scripts
+# NoNewPrivileges support in dirsrv service units requires
+# selinux-policy with init_nnp_daemon_domain for dirsrv_t.
+# See: https://bugzilla.redhat.com/show_bug.cgi?id=2457951
+%if 0%{?rhel} == 10
+Requires: selinux-policy >= 42.1.22-1
+%endif
+%if 0%{?fedora} == 43
+Requires: selinux-policy >= 43.7-1
+%endif
+%if 0%{?fedora} == 44
+Requires: selinux-policy >= 44.1-1
+%endif
+# For CLI scripts
Requires: openldap-clients
Requires: acl
-# this is needed to setup SSL if you are not using the
-# administration server package
+# For SSL/TLS setup
Requires: nss-tools
%dirsrv_requires_ge nss
-# these are not found by the auto-dependency method
-# they are required to support the mandatory LDAP SASL mechs
+# Mandatory LDAP SASL mechanisms
Requires: cyrus-sasl-gssapi
Requires: cyrus-sasl-md5
-# This is optionally supported by us, as we use it in our tests
Requires: cyrus-sasl-plain
-# this is needed for backldbm
+# For back-ldbm
%if %{with libbdb_ro}
Requires: %{name}-robdb-libs = %{version}-%{release}
%else
@@ -482,6 +408,7 @@ Requires: libdb
%endif
%endif
Requires: lmdb-libs
+
# Needed by logconv.pl
%if %{without libbdb_ro}
%if %{without bundle_libdb}
@@ -493,62 +420,44 @@ Requires: perl-Archive-Tar
Requires: perl-debugger
Requires: perl-sigtrap
%endif
+
# Needed for password dictionary checks
Requires: cracklib-dicts
Requires: json-c
-# Log compression
-Requires: zlib-devel
# logconv.py, MIME type
Requires: python3-file-magic
# Picks up our systemd deps.
%{?systemd_requires}
+# Ensure sysusers are created
+%{?sysusers_requires_compat}
+
+%if %{with usdt}
+# Optional eBPF tracer for the shipped USDT bpftrace scripts.
+# Use Suggests so it is not installed by default (debug-only, pulls in gcc).
+Suggests: bpftrace
+%endif
-Source0: https://github.com/389ds/%{name}/releases/download/%{name}-%{version}/%{name}-%{version}.tar.bz2
+
+Source0: %{name}-%{version}.tar.bz2
Source2: %{name}-devel.README
%if %{with bundle_jemalloc}
Source3: https://github.com/jemalloc/%{jemalloc_name}/releases/download/%{jemalloc_ver}/%{jemalloc_name}-%{jemalloc_ver}.tar.bz2
+Source5: jemalloc-5.3.0_throw_bad_alloc.patch
%endif
Source4: 389-ds-base.sysusers
%if %{with bundle_libdb}
-Source5: https://fedorapeople.org/groups/389ds/libdb-5.3.28-59.tar.bz2
-%endif
-
-Source6: vendor-%{version}-1.tar.gz
-Source7: Cargo-%{version}-1.lock
-Source8: cockpit_dist-%{version}-1.tar.bz2
-
-Patch: 0001-Issue-7150-Compressed-access-log-rotations-skipped-a.patch
-Patch: 0002-Sync-lib389-version-to-3.1.4-7161.patch
-Patch: 0003-Issue-7166-db_config_set-asserts-because-of-dynamic-.patch
-Patch: 0004-Issue-7160-Add-lib389-version-sync-check-to-configur.patch
-Patch: 0005-Issue-7096-During-replication-online-total-init-the-.patch
-Patch: 0006-Issue-Revise-paged-result-search-locking.patch
-Patch: 0007-Issue-7108-Fix-shutdown-crash-in-entry-cache-destruc.patch
-Patch: 0008-Issue-7172-Index-ordering-mismatch-after-upgrade-717.patch
-Patch: 0009-Issue-7172-2nd-Index-ordering-mismatch-after-upgrade.patch
-Patch: 0010-Bump-lodash-from-4.17.21-to-4.17.23-in-src-cockpit-3.patch
-Patch: 0011-Issue-7189-DSBLE0007-generates-incorrect-remediation.patch
-Patch: 0012-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch
-Patch: 0013-Issue-7184-argparse.HelpFormatter-_format_actions_us.patch
-Patch: 0014-Issue-7027-2nd-389-ds-base-OpenScanHub-Leaks-Detecte.patch
-Patch: 0015-Issue-7213-MDB_BAD_VALSIZE-error-while-handling-VLV-.patch
-Patch: 0016-Issue-7194-Repl-Log-Analysis-Add-CSN-propagation-det.patch
-Patch: 0017-Issue-7224-CI-Test-Simplify-test_reserve_descriptor_.patch
-Patch: 0018-Issue-7223-Revert-index-scan-limits-for-system-index.patch
-Patch: 0019-Issue-7223-Add-upgrade-function-to-remove-nsIndexIDL.patch
-Patch: 0020-Issue-7223-Add-upgrade-function-to-remove-ancestorid.patch
-Patch: 0021-Issue-7223-Detect-and-log-index-ordering-mismatch-du.patch
-Patch: 0022-Issue-7223-Add-dsctl-index-check-command-for-offline.patch
-Patch: 0023-Issue-7184-2nd-argparse.HelpFormatter-_format_action.patch
-Patch: 0024-Issue-7223-Use-lexicographical-order-for-ancestorid-.patch
-Patch: 0025-Issue-7223-Remove-integerOrderingMatch-requirement-f.patch
-Patch: 0026-Security-fix-for-CVE-2025-14905.patch
-Patch: 0027-Issue-7302-dblib-bdb2mdb-fails-on-F43-F43-upgrade-73.patch
-Patch: 0028-Issue-7267-MDB_BAD_VALSIZE-error-when-updating-index.patch
-Patch: 0029-Issue-7503-CVE-2026-9064-Add-a-limit-to-the-number-c.patch
+Source6: https://fedorapeople.org/groups/389ds/libdb-5.3.28-59.tar.bz2
+%endif
+
+# To override vendor/Cargo.lock/cockpit_dist from the upstream tarball
+# with separately uploaded sources
+# uncomment and replace double percent with a single one
+#Source6: vendor-%%{version}-1.tar.gz
+#Source7: Cargo-%%{version}-1.lock
+#Source8: cockpit_dist-%%{version}-1.tar.bz2
%description
-389 Directory Server is an LDAPv3 compliant server. The base package includes
+389 Directory Server is an LDAPv3 compliant server. The base package includes
the LDAP server and command line utilities for server administration.
%if %{with asan}
WARNING! This build is linked to Address Sanitisation libraries. This probably
@@ -559,7 +468,7 @@ Please see http://seclists.org/oss-sec/2016/q1/363 for more information.
%if %{with libbdb_ro}
%package robdb-libs
Summary: Read-only Berkeley Database Library
-License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT) AND (CC-BY-4.0 AND MIT) AND (MIT OR Apache-2.0) AND Unicode-3.0 AND (MIT OR CC0-1.0) AND (MIT OR Unlicense) AND 0BSD AND Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND ISC AND MIT AND MIT AND ISC AND MPL-2.0 AND PSF-2.0 AND Zlib
+License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT) AND (Apache-2.0 OR MIT) AND Unicode-3.0 AND (CC-BY-4.0 AND MIT) AND (MIT OR Unlicense) AND 0BSD AND Apache-2.0 AND BSD-3-Clause AND ISC AND MIT AND MIT AND ISC AND MPL-2.0 AND Zlib
%description robdb-libs
The %{name}-robdb-lib package contains a library derived from rpm
@@ -570,16 +479,13 @@ some basic functions to search and read Berkeley Database records
%package libs
Summary: Core libraries for 389 Directory Server (%{variant})
-Provides: svrcore = 4.1.4
-Obsoletes: svrcore <= 4.1.3
-Conflicts: svrcore
%dirsrv_requires_ge nss
Requires: nspr
Requires: openldap
Requires: systemd-libs
-# Pull in sasl
+# SASL
Requires: cyrus-sasl-lib
-# KRB
+# Kerberos
Requires: krb5-libs
%if %{with clang}
Requires: llvm
@@ -603,60 +509,71 @@ package to be installed with just the -libs package and without the main package
%package devel
Summary: Development libraries for 389 Directory Server (%{variant})
-Provides: svrcore-devel = 4.1.4
-Obsoletes: svrcore-devel <= 4.1.3
-Conflicts: svrcore-devel
Requires: %{name}-libs = %{version}-%{release}
Requires: pkgconfig
Requires: nspr-devel
Requires: nss-devel >= 3.34
Requires: openldap-devel
-# systemd-libs contains the headers iirc.
+# For systemd headers
Requires: systemd-libs
%description devel
Development Libraries and headers for the 389 Directory Server base package.
+
%package snmp
Summary: SNMP Agent for 389 Directory Server
Requires: %{name} = %{version}-%{release}
-Obsoletes: %{name} <= 1.4.0.0
-
%description snmp
SNMP Agent for the 389 Directory Server base package.
%if %{with bundle_libdb}
%package bdb
Summary: Berkeley Database backend for 389 Directory Server
-%description bdb
-Berkeley Database backend for 389 Directory Server
-Warning! This backend is deprecated in favor of lmdb and its support
-may be removed in future versions.
Requires: %{name} = %{version}-%{release}
+Requires: %{name}-libs = %{version}-%{release}
# Berkeley DB database libdb was marked as deprecated since F40:
# https://fedoraproject.org/wiki/Changes/389_Directory_Server_3.0.0
# because libdb was marked as deprecated since F33
# https://fedoraproject.org/wiki/Changes/Libdb_deprecated
Provides: deprecated()
-%endif
+%description bdb
+Berkeley Database backend for 389 Directory Server
+Warning! This backend is deprecated in favor of lmdb and its support
+may be removed in future versions.
+%endif
%package -n python%{python3_pkgversion}-lib389
Summary: A library for accessing, testing, and configuring the 389 Directory Server
BuildArch: noarch
Requires: %{name} = %{version}-%{release}
Requires: openssl
-# This is for /usr/bin/c_rehash tool, only needed for openssl < 1.1.0
-Requires: openssl-perl
Requires: iproute
Requires: python%{python3_pkgversion}-libselinux
Recommends: bash-completion
%description -n python%{python3_pkgversion}-lib389
This module contains tools and libraries for accessing, testing,
- and configuring the 389 Directory Server.
+and configuring the 389 Directory Server.
+
+%if %{with repl_reports}
+%package -n python%{python3_pkgversion}-lib389-repl-reports
+Summary: HTML and PNG report generation for 389 Directory Server replication monitoring tools
+BuildArch: noarch
+Requires: python%{python3_pkgversion}-lib389 = %{version}-%{release}
+Requires: python%{python3_pkgversion}-plotly
+Requires: python%{python3_pkgversion}-matplotlib
+
+%description -n python%{python3_pkgversion}-lib389-repl-reports
+Extended reporting capabilities for 389 Directory Server replication analysis.
+This package provides additional report formats (HTML and PNG) with interactive
+visualizations and graphs for replication lag analysis. These formats require
+additional dependencies and are optional - the base package supports CSV
+reports without this extension.
+%endif
%if %{with cockpit}
%package -n cockpit-389-ds
@@ -678,7 +595,7 @@ cd src/lib389
%pyproject_buildrequires -g test
%prep
-%autosetup -S git -p1 -n %{name}-%{version}
+%autosetup -p1 -n %{name}-%{version}
%if %{defined SOURCE6}
rm -rf vendor
tar xzf %{SOURCE6}
@@ -692,7 +609,7 @@ cp %{SOURCE7} src/Cargo.lock
%endif
%if %{with bundle_libdb}
-%setup -q -n %{name}-%{version} -T -D -b 5
+%setup -q -n %{name}-%{version} -T -D -b 6
%endif
%if %{defined SOURCE8}
@@ -703,15 +620,10 @@ tar xvjf %{SOURCE8} -C src/cockpit/389-console
cp %{SOURCE2} README.devel
%build
-# Workaround until https://github.com/389ds/389-ds-base/issues/6476 is fixed
-export CFLAGS="%{optflags} -std=gnu17"
-
%if %{with clang}
CLANG_FLAGS="--enable-clang"
%endif
-%{?with_tmpfiles_d: TMPFILES_FLAG="--with-tmpfiles-d=%{with_tmpfiles_d}"}
-
%if %{with asan}
ASAN_FLAGS="--enable-asan --enable-debug"
%endif
@@ -734,6 +646,12 @@ RUST_FLAGS="--enable-rust --enable-rust-offline"
COCKPIT_FLAGS="--disable-cockpit"
%endif
+%if %{with usdt}
+USDT_FLAGS="--enable-usdt"
+%else
+USDT_FLAGS="--disable-usdt"
+%endif
+
%if %{with bundle_jemalloc}
# Override page size, bz #1545539
# 4K
@@ -754,6 +672,7 @@ COCKPIT_FLAGS="--disable-cockpit"
# Build jemalloc
pushd ../%{jemalloc_name}-%{jemalloc_ver}
+patch -p1 -F3 < %{SOURCE5}
%configure \
--libdir=%{_libdir}/%{pkgname}/lib \
--bindir=%{_libdir}/%{pkgname}/bin \
@@ -766,14 +685,16 @@ popd
%if %{with bundle_libdb}
mkdir -p ../%{libdb_base_version}
pushd ../%{libdb_base_version}
-tar -xjf %{_topdir}/SOURCES/%{libdb_full_version}.tar.bz2
+tar -xjf %{_topdir}/SOURCES/%{libdb_full_version}.tar.bz2
mv %{libdb_full_version} SOURCES
-sed -i -e '/^CFLAGS=/s/-fno-strict-aliasing/& -std=gnu99/' %{_builddir}/%{name}-%{version}/rpm/bundle-libdb.spec
-rpmbuild --define "_topdir $PWD" -bc %{_builddir}/%{name}-%{version}/rpm/bundle-libdb.spec
+%if 0%{?fedora}
+sed -i -e '/^CFLAGS=/s/-fno-strict-aliasing/& -std=gnu99/' %{_builddir}/%{name}-%{version}/rpm/bundle-libdb.spec.in
+%endif
+rpmbuild --define "_topdir $PWD" -bc %{_builddir}/%{name}-%{version}/rpm/bundle-libdb.spec.in
popd
%endif
-# Rebuild the autotool artifacts now.
+# Rebuild autotools artifacts
autoreconf -fiv
%configure \
@@ -785,23 +706,19 @@ autoreconf -fiv
%if %{with bundle_libdb}
--with-bundle-libdb=%{_builddir}/%{libdb_base_version}/BUILD/%{libdb_base_dir}/dist/dist-tls \
%endif
- --with-selinux $TMPFILES_FLAG \
+ --with-selinux \
--with-systemd \
--with-systemdsystemunitdir=%{_unitdir} \
--with-systemdsystemconfdir=%{_sysconfdir}/systemd/system \
--with-systemdgroupname=%{groupname} \
--libexecdir=%{_libexecdir}/%{pkgname} \
- $ASAN_FLAGS $MSAN_FLAGS $TSAN_FLAGS $UBSAN_FLAGS $RUST_FLAGS $CLANG_FLAGS $COCKPIT_FLAGS \
-%if 0%{?fedora} >= 34 || 0%{?rhel} >= 9
+ $ASAN_FLAGS $MSAN_FLAGS $TSAN_FLAGS $UBSAN_FLAGS $RUST_FLAGS $CLANG_FLAGS $COCKPIT_FLAGS $USDT_FLAGS \
--with-libldap-r=no \
+%if %{with hibp}
+ --enable-hibp \
%endif
--enable-cmocka
-# Avoid "Unknown key name 'XXX' in section 'Service', ignoring." warnings from systemd on older releases
-%if 0%{?rhel} && 0%{?rhel} < 9
- sed -r -i '/^(Protect(Home|Hostname|KernelLogs)|PrivateMounts)=/d' %{_builddir}/%{name}-%{version}/wrappers/*.service.in
-%endif
-
# lib389
pushd ./src/lib389
%{python3} validate_version.py --update
@@ -809,7 +726,7 @@ pushd ./src/lib389
popd
# Generate symbolic info for debuggers
-export XCFLAGS=$RPM_OPT_FLAGS
+export XCFLAGS="%{optflags}"
%make_build
@@ -832,11 +749,16 @@ sed -i -e "/libback-bdb/d" plugins.list
%endif
# Copy in our docs from doxygen.
-cp -r %{_builddir}/%{name}-%{version}/man/man3 $RPM_BUILD_ROOT/%{_mandir}/man3
+cp -r %{_builddir}/%{name}-%{version}/man/man3 %{buildroot}/%{_mandir}/man3
# lib389
pushd src/lib389
%pyproject_install
+%if 0%{?fedora} <= 41 || (0%{?rhel} && 0%{?rhel} <= 10)
+for clitool in dsconf dscreate dsctl dsidm openldap_to_ds; do
+ mv %{buildroot}%{_bindir}/$clitool %{buildroot}%{_sbindir}/
+done
+%endif
%pyproject_save_files -l lib389
popd
@@ -847,26 +769,26 @@ do
install -p -m 0644 -D -t '%{buildroot}%{bash_completions_dir}' "${clitool}"
done
-mkdir -p $RPM_BUILD_ROOT/var/log/%{pkgname}
-mkdir -p $RPM_BUILD_ROOT/var/lib/%{pkgname}
-mkdir -p $RPM_BUILD_ROOT/var/lock/%{pkgname} \
- && chmod 770 $RPM_BUILD_ROOT/var/lock/%{pkgname}
+mkdir -p %{buildroot}/var/log/%{pkgname}
+mkdir -p %{buildroot}/var/lib/%{pkgname}
+mkdir -p %{buildroot}/var/lock/%{pkgname} \
+ && chmod 770 %{buildroot}/var/lock/%{pkgname}
-# for systemd
-mkdir -p $RPM_BUILD_ROOT%{_sysconfdir}/systemd/system/%{groupname}.wants
+# For systemd
+mkdir -p %{buildroot}%{_sysconfdir}/systemd/system/%{groupname}.wants
install -p -D -m 0644 %{SOURCE4} %{buildroot}%{_sysusersdir}/389-ds-base.conf
-#remove libtool and static libs
-rm -f $RPM_BUILD_ROOT%{_libdir}/%{pkgname}/*.a
-rm -f $RPM_BUILD_ROOT%{_libdir}/%{pkgname}/*.la
-rm -f $RPM_BUILD_ROOT%{_libdir}/%{pkgname}/plugins/*.a
-rm -f $RPM_BUILD_ROOT%{_libdir}/%{pkgname}/plugins/*.la
-rm -f $RPM_BUILD_ROOT%{_libdir}/libsvrcore.a
-rm -f $RPM_BUILD_ROOT%{_libdir}/libsvrcore.la
+# Remove libtool and static libs
+rm -f %{buildroot}%{_libdir}/%{pkgname}/*.a
+rm -f %{buildroot}%{_libdir}/%{pkgname}/*.la
+rm -f %{buildroot}%{_libdir}/%{pkgname}/plugins/*.a
+rm -f %{buildroot}%{_libdir}/%{pkgname}/plugins/*.la
+rm -f %{buildroot}%{_libdir}/libsvrcore.a
+rm -f %{buildroot}%{_libdir}/libsvrcore.la
%if %{with bundle_jemalloc}
pushd ../%{jemalloc_name}-%{jemalloc_ver}
-make DESTDIR="$RPM_BUILD_ROOT" install_lib install_bin
+make DESTDIR="%{buildroot}" install_lib install_bin
cp -pa COPYING ../%{name}-%{version}/COPYING.jemalloc
cp -pa README ../%{name}-%{version}/README.jemalloc
popd
@@ -879,7 +801,7 @@ libdbdestdir=$PWD/../%{name}-%{version}
cp -pa $libdbbuilddir/LICENSE $libdbdestdir/LICENSE.libdb
cp -pa $libdbbuilddir/README $libdbdestdir/README.libdb
cp -pa $libdbbuilddir/lgpl-2.1.txt $libdbdestdir/lgpl-2.1.txt.libdb
-cp -pa $libdbbuilddir/dist/dist-tls/.libs/%{libdb_bundle_name} $RPM_BUILD_ROOT%{_libdir}/%{pkgname}/%{libdb_bundle_name}
+cp -pa $libdbbuilddir/dist/dist-tls/.libs/%{libdb_bundle_name} %{buildroot}%{_libdir}/%{pkgname}/%{libdb_bundle_name}
popd
%endif
@@ -900,12 +822,12 @@ popd
%endif
%check
-# This checks the code, if it fails it prints why, then re-raises the fail to shortcircuit the rpm build.
+# Run cmocka unit tests
%if %{with tsan}
export TSAN_OPTIONS=print_stacktrace=1:second_deadlock_stack=1:history_size=7
%endif
%if %{without asan} && %{without msan}
-if ! make DESTDIR="$RPM_BUILD_ROOT" check; then cat ./test-suite.log && false; fi
+if ! make DESTDIR="%{buildroot}" check; then cat ./test-suite.log && false; fi
%endif
# Check import for lib389 modules
@@ -914,19 +836,17 @@ if ! make DESTDIR="$RPM_BUILD_ROOT" check; then cat ./test-suite.log && false; f
%post
if [ -n "$DEBUGPOSTTRANS" ] ; then
output=$DEBUGPOSTTRANS
- output2=${DEBUGPOSTTRANS}.upgrade
else
output=/dev/null
- output2=/dev/null
fi
-# reload to pick up any changes to systemd files
+# Reload to pick up any changes to systemd files
/bin/systemctl daemon-reload >$output 2>&1 || :
-# Reload our sysctl before we restart (if we can)
-sysctl --system &> $output; true
+# Reload sysctl before restarting instances
+sysctl --system &> "$output"; true
-# Gather running instances, stop them, then restart
+# Gather running instances, stop and restart them
instbase="%{_sysconfdir}/%{pkgname}"
instances=""
ninst=0
@@ -937,7 +857,6 @@ for dir in "$instbase"/slapd-* ; do
case "$dir" in *.removed) continue ;; esac
basename=$(basename "$dir")
inst="%{pkgname}@${basename#slapd-}"
- inst_name="${basename#slapd-}"
echo "found instance $inst - getting status" >> "$output" 2>&1 || :
if /bin/systemctl -q is-active "$inst" ; then
echo "instance $inst is running - stopping for upgrade" >> "$output" 2>&1 || :
@@ -980,7 +899,6 @@ fi
%postun snmp
%systemd_postun_with_restart %{pkgname}-snmp.service
-exit 0
%files -f plugins.list
%if %{with bundle_jemalloc}
@@ -994,7 +912,7 @@ exit 0
%config(noreplace)%{_sysconfdir}/%{pkgname}/schema/*.ldif
%dir %{_sysconfdir}/%{pkgname}/config
%dir %{_sysconfdir}/systemd/system/%{groupname}.wants
-%{_sysusersdir}/389-ds-base.conf
+%{_sysusersdir}/%{name}.conf
%config(noreplace)%{_sysconfdir}/%{pkgname}/config/slapd-collations.conf
%config(noreplace)%{_sysconfdir}/%{pkgname}/config/certmap.conf
%{_datadir}/%{pkgname}
@@ -1028,8 +946,6 @@ exit 0
%{_mandir}/man5/dirsrv.systemd.5.gz
%{_libdir}/%{pkgname}/python
%dir %{_libdir}/%{pkgname}/plugins
-# This has to be hardcoded to /lib - $libdir changes between lib/lib64, but
-# sysctl.d is always in /lib.
%{_prefix}/lib/sysctl.d/*
%dir %{_localstatedir}/lib/%{pkgname}
%dir %{_localstatedir}/log/%{pkgname}
@@ -1093,11 +1009,19 @@ exit 0
%doc src/lib389/README.md
%license LICENSE LICENSE.GPLv3+
# Binaries
+%if 0%{?fedora} >= 42 || 0%{?rhel} >= 11
%{_bindir}/dsconf
%{_bindir}/dscreate
%{_bindir}/dsctl
%{_bindir}/dsidm
%{_bindir}/openldap_to_ds
+%else
+%{_sbindir}/dsconf
+%{_sbindir}/dscreate
+%{_sbindir}/dsctl
+%{_sbindir}/dsidm
+%{_sbindir}/openldap_to_ds
+%endif
%{_libexecdir}/%{pkgname}/dscontainer
# Man pages
%{_mandir}/man8/dsconf.8.gz
@@ -1112,6 +1036,11 @@ exit 0
%{bash_completions_dir}/dscreate
%{bash_completions_dir}/dsidm
+%if %{with repl_reports}
+%files -n python%{python3_pkgversion}-lib389-repl-reports
+# No files needed as this is just a meta-package for dependencies
+%endif
+
%if %{with cockpit}
%files -n cockpit-389-ds -f cockpit.list
%{_datarootdir}/metainfo/389-console/org.port389.cockpit_console.metainfo.xml
diff --git a/389-ds-base.sysusers b/389-ds-base.sysusers
index 32a3452..4411113 100644
--- a/389-ds-base.sysusers
+++ b/389-ds-base.sysusers
@@ -1,3 +1,3 @@
#Type Name ID GECOS Home directory Shell
g dirsrv 389
-u dirsrv 389:389 "user for 389-ds-base" /usr/share/dirsrv/ /sbin/nologin
+u dirsrv 389:dirsrv "user for 389-ds-base" /usr/share/dirsrv/ /sbin/nologin
diff --git a/jemalloc-5.3.0_throw_bad_alloc.patch b/jemalloc-5.3.0_throw_bad_alloc.patch
new file mode 100644
index 0000000..94e4d36
--- /dev/null
+++ b/jemalloc-5.3.0_throw_bad_alloc.patch
@@ -0,0 +1,41 @@
+#commit 3de0c24859f4413bf03448249078169bb50bda0f
+#Author: divanorama <divanorama@gmail.com>
+#Date: Thu Sep 29 23:35:59 2022 +0200
+#
+# Disable builtin malloc in tests
+#
+# With `--with-jemalloc-prefix=` and without `-fno-builtin` or `-O1` both clang and gcc may optimize out `malloc` calls
+# whose result is unused. Comparing result to NULL also doesn't necessarily count as being used.
+#
+# This won't be a problem in most client programs as this only concerns really unused pointers, but in
+# tests it's important to actually execute allocations.
+# `-fno-builtin` should disable this optimization for both gcc and clang, and applying it only to tests code shouldn't hopefully be an issue.
+# Another alternative is to force "use" of result but that'd require more changes and may miss some other optimization-related issues.
+#
+# This should resolve https://github.com/jemalloc/jemalloc/issues/2091
+#
+#diff --git a/Makefile.in b/Makefile.in
+#index 6809fb29..a964f07e 100644
+#--- a/Makefile.in
+#+++ b/Makefile.in
+#@@ -458,6 +458,8 @@ $(TESTS_OBJS): $(objroot)test/%.$(O): $(srcroot)test/%.c
+# $(TESTS_CPP_OBJS): $(objroot)test/%.$(O): $(srcroot)test/%.cpp
+# $(TESTS_OBJS): CPPFLAGS += -I$(srcroot)test/include -I$(objroot)test/include
+# $(TESTS_CPP_OBJS): CPPFLAGS += -I$(srcroot)test/include -I$(objroot)test/include
+#+$(TESTS_OBJS): CFLAGS += -fno-builtin
+#+$(TESTS_CPP_OBJS): CPPFLAGS += -fno-builtin
+# ifneq ($(IMPORTLIB),$(SO))
+# $(CPP_OBJS) $(C_SYM_OBJS) $(C_OBJS) $(C_JET_SYM_OBJS) $(C_JET_OBJS): CPPFLAGS += -DDLLEXPORT
+# endif
+diff --git a/src/jemalloc_cpp.cpp b/src/jemalloc_cpp.cpp
+index fffd6aee..5a682991 100644
+--- a/src/jemalloc_cpp.cpp
++++ b/src/jemalloc_cpp.cpp
+@@ -93,7 +93,7 @@ handleOOM(std::size_t size, bool nothrow) {
+ }
+
+ if (ptr == nullptr && !nothrow)
+- std::__throw_bad_alloc();
++ throw std::bad_alloc();
+ return ptr;
+ }
diff --git a/main.fmf b/main.fmf
index 76d16bf..bd58518 100644
--- a/main.fmf
+++ b/main.fmf
@@ -10,7 +10,7 @@
package: [389-ds-base, git, pytest]
- name: clone repo
how: shell
- script: git clone https://github.com/389ds/389-ds-base /root/ds
+ script: git clone -b 389-ds-base-3.1 https://github.com/389ds/389-ds-base /root/ds
/test:
/upstream_basic:
test: pytest -v /root/ds/dirsrvtests/tests/suites/basic/basic_test.py
diff --git a/sources b/sources
index b6b2faf..0058c74 100644
--- a/sources
+++ b/sources
@@ -1,6 +1,3 @@
SHA512 (jemalloc-5.3.0.tar.bz2) = 22907bb052096e2caffb6e4e23548aecc5cc9283dce476896a2b1127eee64170e3562fa2e7db9571298814a7a2c7df6e8d1fbe152bd3f3b0c1abec22a2de34b1
SHA512 (libdb-5.3.28-59.tar.bz2) = 731a434fa2e6487ebb05c458b0437456eb9f7991284beb08cb3e21931e23bdeddddbc95bfabe3a2f9f029fe69cd33a2d4f0f5ce6a9811e9c3b940cb6fde4bf79
-SHA512 (389-ds-base-3.1.4.tar.bz2) = 17de77a02c848dbb8d364e7bab529726b4c32e466f47d5c2a5bba8d8b55e2a56e2b743a2efa4f820c935b39f770a621146a42443e4f171f8b14c68968155ee2c
-SHA512 (vendor-3.1.4-1.tar.gz) = 52fcb3268b863a4b8d3fe61109ab8ef231d7fc412768d8019244bc245cfface20c4531195d08d29577eaca29e87046e74083b8d33f2fb4cc0b16a6d734a854af
-SHA512 (Cargo-3.1.4-1.lock) = 949013fe4cfe30969b8411b465901514c3ea5144d329a4ea1d80244e4bd3d000db857f9b78d57a4f4f3fd240ec5b7f91ed8c39ff53a718c911ee205410bb7cc7
-SHA512 (cockpit_dist-3.1.4-1.tar.bz2) = faeaa67801f9d61f74decbc989fc05c2f6f1242879d0202482d13d5071562e529827aec78a4d9f2e19684026c5f0a2d47a4e83120b035596da2951792059ba18
+SHA512 (389-ds-base-3.1.5.tar.bz2) = 00b3abeab03b256907999082f62261831908717e380131ec4782cc8e35f9322baac72a6d760b9877b88c570f16bc6586cb302b35aa0c9c38a8b7b076febcbaf5
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-09-07 17:29 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-07 17:29 [rpms/389-ds-base] f43: Bump version to 3.1.5 Viktor Ashirov
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox