Problem
A UCS@school environment experienced unusually long CSV user import runtimes. Imports were started shortly before midnight after CSV preparation and ordinarily took up to two hours. During the affected period, individual imports reportedly took 5–15 hours. The administrator observed multi-minute pauses when new school classes were created and suspected that creating class shares on remote UCS Replica servers might be responsible.
Comparison of annual import runs
| Metric | Previous year | Current year |
|---|---|---|
| School holiday period import started | 2025-07-14_22:02:21 |
2026-07-19_23:40:32 |
| Import completed | 2025-07-15_05:28:35 |
2026-07-21_15:32:05 |
| Modified | 103,925 | 103,292 |
| Added | 4,665 | 4,847 |
| Removed | 17,802 | 16,090 |
| Elapsed time | 7 h 26 min 14 s | 39 h 51 min 33 s |
The reported 5–15-hour daily import times and the nearly 40-hour comparison run refer to different observations. They should not be treated as the same measurement.
Root cause
Investigation of class and class-share creation identified an LDAP search using univentionObjectIdentifier. This attribute was missing from the configured equality index. An unindexed subtree search across a large LDAP directory can be expensive, particularly when repeated during object creation. The logs and UCR output support the missing-index diagnosis; the traces alone do not establish that every minute of the class-share delay was spent in this one LDAP search.
The LDAP trace showed:
04.09.26 15:29:00.100 LDAP ( ALL ) : search | filter=univentionObjectIdentifier=42f78224-5964-4b6d-a011-c77748f4a1d0 base=dc=example,dc=org scope=sub attributes=['dn'] unique=False required=False timeout=-1 sizelimit=0
A configuration check returned zero:
root@primary:~/univention-support # ucr dump | grep ^ldap/index/* | grep -c univentionObjectIdentifier
0
An environment where the attribute is present in the configured index lists returned:
root@ucs5primary:~# ucr dump | grep ^ldap/index/* | grep -c univentionObjectIdentifier
1
This check searches all matching UCR index settings, not specifically the equality index; use ucr get ldap/index/eq to verify the required equality-index configuration. The UCR value alone does not prove that the physical database index has been built.
Solution
Important: Index rebuilding requires a maintenance window and an LDAP service interruption. Depending on directory size, it may take several hours. Back up the LDAP database and current configuration according to your operational recovery procedure before making changes. Run the commands on the LDAP server that hosts the affected database and verify replication and dependent services afterward.
1. Inspect the equality-index configuration
ucr get ldap/index/eq
Expected default output in the documented environment:
aRecord,automountInformation,cNAMERecord,cn,description,dhcpHWAddress,displayName,entryUUID,gidNumber,givenName,homeDirectory,krb5PrincipalName,krb5ValidStart,macAddress,mail,mailAlternativeAddress,mailPrimaryAddress,memberUid,objectClass,ou,pTRRecord,relativeDomainName,sambaAcctFlags,sambaDomainName,sambaGroupType,sambaPrimaryGroupSID,sambaSID,sambaSIDList,secretary,shadowExpire,sn,ucsschoolLegalGuardian,ucsschoolLegalWard,ucsschoolRecordUID,ucsschoolRole,ucsschoolSchool,ucsschoolSourceUID,uid,uidNumber,uniqueMember,univentionAppID,univentionCanonicalRecipientRewriteEnabled,univentionDataType,univentionInventoryNumber,univentionLicenseModule,univentionLicenseObject,univentionMailHomeServer,univentionNagiosHostname,univentionObjectFlag,univentionObjectIdentifier,univentionObjectType,univentionPolicyReference,univentionServerRole,univentionService,univentionShareGid,univentionShareSambaName,univentionShareWriteable,univentionUDMOptionModule,univentionUDMPropertyCLIName,univentionUDMPropertyCopyable,univentionUDMPropertyDefault,univentionUDMPropertyDeleteObjectClass,univentionUDMPropertyDoNotSearch,univentionUDMPropertyHook,univentionUDMPropertyLayoutOverwritePosition,univentionUDMPropertyLayoutOverwriteTab,univentionUDMPropertyLayoutPosition,univentionUDMPropertyLayoutTabAdvanced,univentionUDMPropertyLayoutTabName,univentionUDMPropertyLdapMapping,univentionUDMPropertyLongDescription,univentionUDMPropertyModule,univentionUDMPropertyMultivalue,univentionUDMPropertyObjectClass,univentionUDMPropertyOptions,univentionUDMPropertyShortDescription,univentionUDMPropertySyntax,univentionUDMPropertyTranslationLongDescription,univentionUDMPropertyTranslationShortDescription,univentionUDMPropertyTranslationTabName,univentionUDMPropertyValueMayChange,univentionUDMPropertyValueRequired,univentionUDMPropertyVersion,zoneName
Check whether univentionObjectIdentifier is present. Preserve any site-specific attributes in the existing value rather than blindly replacing the entire list with the example default.
2. Add the missing attribute
If the attribute is missing, the following command sets the complete example equality-index list. Use it only if this list is appropriate for your installation; otherwise append the missing attribute to the existing value.
ucr set ldap/index/eq="aRecord,automountInformation,cNAMERecord,cn,description,dhcpHWAddress,displayName,entryUUID,gidNumber,givenName,homeDirectory,krb5PrincipalName,krb5ValidStart,macAddress,mail,mailAlternativeAddress,mailPrimaryAddress,memberUid,objectClass,ou,pTRRecord,relativeDomainName,sambaAcctFlags,sambaDomainName,sambaGroupType,sambaPrimaryGroupSID,sambaSID,sambaSIDList,secretary,shadowExpire,sn,ucsschoolLegalGuardian,ucsschoolLegalWard,ucsschoolRecordUID,ucsschoolRole,ucsschoolSchool,ucsschoolSourceUID,uid,uidNumber,uniqueMember,univentionAppID,univentionCanonicalRecipientRewriteEnabled,univentionDataType,univentionInventoryNumber,univentionLicenseModule,univentionLicenseObject,univentionMailHomeServer,univentionNagiosHostname,univentionObjectFlag,univentionObjectIdentifier,univentionObjectType,univentionPolicyReference,univentionServerRole,univentionService,univentionShareGid,univentionShareSambaName,univentionShareWriteable,univentionUDMOptionModule,univentionUDMPropertyCLIName,univentionUDMPropertyCopyable,univentionUDMPropertyDefault,univentionUDMPropertyDeleteObjectClass,univentionUDMPropertyDoNotSearch,univentionUDMPropertyHook,univentionUDMPropertyLayoutOverwritePosition,univentionUDMPropertyLayoutOverwriteTab,univentionUDMPropertyLayoutPosition,univentionUDMPropertyLayoutTabAdvanced,univentionUDMPropertyLayoutTabName,univentionUDMPropertyLdapMapping,univentionUDMPropertyLongDescription,univentionUDMPropertyModule,univentionUDMPropertyMultivalue,univentionUDMPropertyObjectClass,univentionUDMPropertyOptions,univentionUDMPropertyShortDescription,univentionUDMPropertySyntax,univentionUDMPropertyTranslationLongDescription,univentionUDMPropertyTranslationShortDescription,univentionUDMPropertyTranslationTabName,univentionUDMPropertyValueMayChange,univentionUDMPropertyValueRequired,univentionUDMPropertyVersion,zoneName"
Verify:
ucr dump | grep ^ldap/index/* | grep -c univentionObjectIdentifier
Expected result for the example configuration:
1
3. Build the physical index
Changing the UCR value alone does not necessarily populate the database index. In the documented case, the missing attribute was indexed individually:
systemctl stop slapdslapindex -f /etc/ldap/slapd.conf univentionObjectIdentifiersystemctl start slapd
Confirm that slapindex completed successfully before starting slapd; inspect the command output, service status and LDAP logs. Run slapindex as the appropriate account for the local deployment and check database ownership afterward if required. Do not run a manual rebuild concurrently with an automatic rebuild.
4. Validate the result
Confirm the equality-index setting, successful LDAP startup, expected LDAP operations:
grep -i univentionobjectidentifier /usr/share/univention-ldap/ldap_setup_index
root@ucs5primary:~# grep -i univentionobjectidentifier /usr/share/univention-ldap/ldap_setup_index
'univentionObjectIdentifier',
(Optional): rebuild all LDAP indexes
The investigation also documented the following full rebuild procedure:
# Stop LDAP service
service slapd stop
# Rebuild indexes
slapindex
# Start LDAP service
service slapd start
A full rebuild may take considerably longer than rebuilding a single attribute. Choose one supported rebuild procedure appropriate to the installed UCS/OpenLDAP version, not both without reason.
Investigation
Class creation workflow
- The user import processes a user and determines the required groups.
- For each group,
get_or_create_group_udm_object()is called inucs-school-lib/modules/ucsschool/lib/models/user.py:641-675. - A missing class causes creation of a
SchoolClassobject. SchoolClass.create_without_hooks()first creates the LDAP/UDM group object (ucs-school-lib/modules/ucsschool/lib/models/group.py:228-232).- By default, the class share is then created:
if self._create_share and self.exists(lo):
success = success and self.create_share(self.get_machine_connection())
create_share()constructs aClassShareand saves it using the machine connection (ucs-school-lib/modules/ucsschool/lib/models/group.py:234-236).
Additional class-share operations
The class-share creation process retrieves the school’s file server from LDAP; may select a remote UCS Slave/Replica as the share server; sets the server FQDN, share path and Unix/Samba permissions; reads the group GID; constructs NT ACLs from the group SID; and creates a shares/share UDM object. Relevant source locations are ucs-school-lib/modules/ucsschool/lib/models/share.py:170-187, :212-253, and :279-313.
Server selection may involve LDAP queries for the OU, server object and univentionLDAPAccessWrite. Slow LDAP, DNS, Samba or remote-server operations can also contribute to latency and should be investigated if indexing does not resolve the problem.
Timed reproduction of a class creation
root@primary:~/univention-support # time python3 -m ucsschool.lib.models --debug create SchoolClass --name ge164161-1g --school ge164161
2026-09-04 16:38:20 DEBUG __main__.create:235 create: model='SchoolClass' name='ge164161-1g' school='ge164161' multi_value=() single_value=()
2026-09-04 16:38:20 DEBUG base.get_only_udm_obj:1096 Getting SchoolClass UDM object by filter: name=ge164161-1g
2026-09-04 16:38:20 DEBUG base.call_hooks:478 Starting SchoolClass.call_hooks('pre', 'create', lo('cn=admin,dc=example,dc=org')) for SchoolClass(name='ge164161-1g', school='ge164161', dn='cn=ge164161-1g,cn=klassen,cn=schueler,cn=group
hule,dc=org').
2026-09-04 16:38:20 INFO pyhooks_loader.get_hook_classes:101 Searching for hooks of type 'Hook' in: /var/lib/ucs-school-lib/hooks...
2026-09-04 16:38:20 INFO pyhooks_loader.get_hook_classes:127 Found hook classes: EnableKiksUser
2026-09-04 16:38:20 INFO pyhooks_loader.get_hook_objects:185 Loaded hooks: {'post_create': ['EnableKiksUser.post_create']}.
2026-09-04 16:38:20 INFO base.create_without_hooks:534 Creating SchoolClass(name='ge164161-1g', school='ge164161', dn='cn=ge164161-1g,cn=klassen,cn=schueler,cn=groups,ou=ge164161,dc=example,dc=org')
2026-09-04 16:38:20 DEBUG base.get_udm_object:823 Getting School UDM object by dn: ou=ge164161,dc=example,dc=org
2026-09-04 16:38:25 INFO base.create_without_hooks:560 SchoolClass(name='ge164161-1g', school='ge164161', dn='cn=ge164161-1g,cn=klassen,cn=schueler,cn=groups,ou=ge164161,dc=example,dc=org') successfully created
2026-09-04 16:38:25 DEBUG base.get_only_udm_obj:1096 Getting SchoolClass UDM object by filter: name=ge164161-1g
2026-09-04 16:38:25 DEBUG base.get_udm_object:823 Getting ClassShare UDM object by dn: cn=ge164161-1g,cn=klassen,cn=shares,ou=ge164161,dc=example,dc=org
2026-09-04 16:38:25 DEBUG base.call_hooks:478 Starting ClassShare.call_hooks('pre', 'create', lo('cn=primary,cn=dc,cn=computers,dc=example,dc=org')) for ClassShare(name='ge164161-1g', school='ge164161', dn='cn=ge164161-1g,cn=klassen,cn
,dc=example,dc=org').
2026-09-04 16:38:25 INFO base.create_without_hooks:534 Creating ClassShare(name='ge164161-1g', school='ge164161', dn='cn=ge164161-1g,cn=klassen,cn=shares,ou=ge164161,dc=example,dc=org')
2026-09-04 16:38:25 DEBUG base.get_udm_object:823 Getting School UDM object by dn: ou=ge164161,dc=example,dc=org
2026-09-04 16:38:25 DEBUG base.from_dn:1069 Looking up School with dn 'ou=ge164161,dc=example,dc=org'
2026-09-04 16:38:25 DEBUG base.get_only_udm_obj:1096 Getting SchoolClass UDM object by filter: name=ge164161-1g
2026-09-04 16:38:25 INFO share.do_create:186 Creating share on "school-fileserver.example.org"
2026-09-04 16:40:27 INFO base.create_without_hooks:560 ClassShare(name='ge164161-1g', school='ge164161', dn='cn=ge164161-1g,cn=klassen,cn=shares,ou=ge164161,dc=example,dc=org') successfully created
2026-09-04 16:40:27 DEBUG base.call_hooks:478 Starting ClassShare.call_hooks('post', 'create', lo('cn=primary,cn=dc,cn=computers,dc=example,dc=org')) for ClassShare(name='ge164161-1g', school='ge164161', dn='cn=ge164161-1g,cn=klassen,c
1,dc=example,dc=org').
2026-09-04 16:40:27 DEBUG base.call_hooks:478 Starting SchoolClass.call_hooks('post', 'create', lo('cn=admin,dc=example,dc=org')) for SchoolClass(name='ge164161-1g', school='ge164161', dn='cn=ge164161-1g,cn=klassen,cn=schueler,cn=grou
hule,dc=org').
real 2m7,809s
user 0m1,118s
sys 0m0,120s
The group object was created by 16:38:25, but the share creation started at 16:38:25 and completed at 16:40:27. The complete command took 2 min 7.809 s, with very little user or system CPU time. This establishes that the observed wait occurred during the share-creation phase, but not which underlying call accounted for all of it.
Debugging script and command
The investigation used the following script (entered as vim testshare.py):
import logging, univention.logging
univention.logging.basicConfig(
filename="stdout", level=logging.DEBUG,
univention_debug_categories=["ADMIN", "LDAP"],
use_structured_logging=False)
from ucsschool.lib.models.__main__ import cli
cli(["--debug", "create", "SchoolClass", "--name", "ge164161-1d", "--school", "ge164161"], obj={})
vim testshare.py
time python3 testShare.py 2>&1 | tee -a testShare.log
Note: The supplied script filename uses testshare.py while the execution command uses testShare.py. On case-sensitive filesystems, use the same spelling for both.
Relevant LDAP debug output:
04.09.26 15:29:00.093 LDAP ( ALL ) : add | dn=cn=ge164161-1d,cn=groupName,cn=temporary,cn=univention,dc=example,dc=org addlist=[('objectClass', [b'top', b'lock']), ('cn', [b'ge164161-1d']), ('lockTime', [b'1788528840'])]
04.09.26 15:29:00.093 LDAP ( ALL ) : add | dn=cn=ge164161-1d,cn=groupName,cn=temporary,cn=univention,dc=example,dc=org
04.09.26 15:29:00.095 LDAP ( ALL ) : search | filter=(|(&(cn=ge164161-1d)(|(objectClass=univentionGroup)(objectClass=sambaGroupMapping)(objectClass=posixGroup)))(uid=ge164161-1d)) base=dc=example,dc=org scope=sub attributes
e required=False timeout=-1 sizelimit=0
04.09.26 15:29:00.097 LDAP ( ALL ) : search | filter=(|(uid=ge164161-1d)) base=cn=klassen,cn=schueler,cn=groups,ou=ge164161,dc=example,dc=org scope=one attributes=['dn'] unique=False required=False timeout=-1 sizelimit=0
04.09.26 15:29:00.099 LDAP ( ALL ) : add | dn=cn=42f78224-5964-4b6d-a011-c77748f4a1d0,cn=univentionObjectIdentifier,cn=temporary,cn=univention,dc=example,dc=org addlist=[('objectClass', [b'top', b'lock']), ('cn', [b'42f7822
7748f4a1d0']), ('lockTime', [b'1788528840'])]
04.09.26 15:29:00.099 LDAP ( ALL ) : add | dn=cn=42f78224-5964-4b6d-a011-c77748f4a1d0,cn=univentionObjectIdentifier,cn=temporary,cn=univention,dc=example,dc=org
04.09.26 15:29:00.100 LDAP ( ALL ) : search | filter=univentionObjectIdentifier=42f78224-5964-4b6d-a011-c77748f4a1d0 base=dc=example,dc=org scope=sub attributes=['dn'] unique=False required=False timeout=-1 sizelimit=0
04.09.26 15:29:01.337 LDAP ( ALL ) : add | dn=cn=84609,cn=gidNumber,cn=temporary,cn=univention,dc=example,dc=org addlist=[('objectClass', [b'top', b'lock']), ('cn', [b'84609']), ('lockTime', [b'1788528841'])]
Count the configured LDAP attributes
# ucr search --brief ldap/index/{eq,pres,sub} | cut -d: -f2 | tr ',' ' ' | sed -re 's/ +/ /' | tr " " "\n" | sort -u
[.... List of all LDAP indexes with duplicates removed ...]
# ucr search --brief ldap/index/{eq,pres,sub} | cut -d: -f2 | tr ',' ' ' | sed -re 's/ +/ /' | tr " " "\n" | sort -u | wc -l
124
One hypothesis was that an earlier index limit of approximately 128 attributes had motivated omission of univentionObjectIdentifier. The investigation states that a later erratum increased the supported limit to 256, but gives only the placeholder Erratum XYZ. Therefore the exact release, applicability and current limit must be verified for the deployed UCS version; the hypothesis is not established as the historical cause.
Minimal class-creation test
python3 -c '
import logging, univention.logging
univention.logging.basicConfig(
filename="stdout", level=logging.DEBUG,
univention_debug_categories=["ADMIN", "LDAP"],
use_structured_logging=False)
from ucsschool.lib.models.__main__ import cli
cli(["--debug", "create", "SchoolClass", "--name", "DEMOSCHOOL-1a", "--school", "DEMOSCHOOL"], obj={})
'
Caution: This creates real directory objects. Use an appropriate test school/class name and clean up test objects through supported UDM/UCS@school procedures.
Where the LDAP indexes are defined
1. UCR variable declarations: management/univention-ldap/debian/univention-ldap-server.univention-config-registry-variables documents ldap/index/eq, ldap/index/pres, ldap/index/sub, ldap/index/approx, ldap/index/autorebuild and ldap/index/quickmode. This declaration file does not contain the actual default attribute lists.
2. Recommended default attributes: management/univention-ldap/scripts/ldap_setup_index contains the RECOMMENDED_LDAP_INDEX dictionary with eq, pres, sub and approx entries. For example:
'approx': {
'cn',
'givenName',
'mail',
'sn',
'uid',
}
The script reads ldap/index/<type> UCR values and can write changes through ucr_update(ucr, changes). An example invocation is:
/usr/share/univention-ldap/ldap_setup_index --add-defaults
The investigation confirmed the recommended attribute in the script:
root@primary:~/univention-support # grep -i univentionobjectidentifier /usr/share/univention-ldap/ldap_setup_index
'univentionObjectIdentifier',
3. OpenLDAP configuration generation: management/univention-ldap/conffiles/etc/ldap/slapd.conf.d/40univention-ldap-server_database converts UCR attribute lists into index directives:
attr2index = defaultdict(list)
for indextype in ('pres', 'eq', 'sub', 'approx'):
value = configRegistry.get("ldap/index/" + indextype, "").strip()
if value == "none":
continue
for attr in value.split(","):
attr2index[attr].append(indextype)
for attr, indextypes in sorted(attr2index.items()):
print("index\t%s\t%s" % (attr, ",".join(indextypes)))
For example, ldap/index/eq=cn,uid, ldap/index/pres=cn,uid and ldap/index/sub=cn produce approximately:
index cn eq,pres,sub
index uid eq,pres
The configuration path is:
UCR ldap/index/*
|
v
40univention-ldap-server_database
|
v
Generated /etc/ldap/slapd.conf
|
v
OpenLDAP uses the index directives
The investigation recommends preferring ldap_setup_index for index configuration because it also validates configuration and, with ldap/index/autorebuild enabled, can initiate slapindex. Review its version-specific behavior before combining it with a manual rebuild.
Conclusion
The observed slowdown coincided with class-share creation and an absent univentionObjectIdentifier LDAP equality index. Restoring the configured attribute and building its physical index addresses a plausible, concrete performance bottleneck. The material provided does not include a measured post-fix import runtime, so a quantitative improvement cannot be claimed without follow-up benchmarking.