Problem
Following a recent upgrade to Univention Corporate Server (UCS) 5.2-5 errata483, the system diagnostics reported a critical error regarding the domain join status. The command univention-check-join-status returned an error indicating a failure in the domain join process.
STDOUT:
Warning: 'univention-directory-manager-rest' is not configured.
Error: Not all install files configured: 1 missing
Traceback:
RUNNING 22univention-directory-manager-rest.inst
2026-05-08 16:06:38.193766072+02:00 (in joinscript_init)
Not updating directory/manager/ast/authorized-groups/domain-admins
Not updating directory/manager/rest/authorized-groups/dc-backup
Not updating directory/manager/rest/authorized-groups/dc-slaves
Object exists: cn=services,cn=univention,dc=univention,dc=de
Object exists: cn=Univention Directory Manager REST,cn=services,cn=univention,dc=univention,dc=de
No modification: cn=x100,cn=dc,cn=computers,dc=univention,dc=de
WARNING: cannot append Univention Directory Manager REST to service, value exists
Traceback (most recent call last):
File "/usr/share/univention-directory-manager-tools/univention-cli-server", line 204, in doit
univention.admincli.admin.main(arglist, stdout, stderr)
File "/usr/lib/python3/dist-packages/univention/admincli/admin.py", line 374, in main
_doit(arglist, stdout, stderr)
File "/usr/lib/python3/dist-packages/univention/admincli/admin.py", line 636, in _doit
cli.create(input, append, ignore_exists, parsed_options, parsed_append_options, parsed_remove_options, policy_reference)
File "/usr/lib/python3/dist-packages/univention/admincli/admin.py", line 679, in create
return self._create(self.module_name, self.module, self.dn, self.lo, self.position, self.superordinate, _args,_ *kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/univention/admin/handlers/__init__.py", line 658, in create
dn = object.create()
^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/univention/admin/handlers/__init__.py", line 748, in _create
dn = self._create(response=response, serverctrls=serverctrls, ignore_license=ignore_license)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/univention/admin/handlers/__init__.py", line 1547, in _create
raise exc[1].with_traceback(exc[2])
File "/usr/lib/python3/dist-packages/univention/admin/handlers/__init__.py", line 1529, in _create
self.lo.authz_connection.add(self.dn, al, serverctrls=serverctrls, response=response, ignore_license=ignore_license)
File "/usr/lib/python3/dist-packages/univention/admin/uldap.py", line 616, in add
return self.lo.add(dn, al, serverctrls=serverctrls, response=response)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/univention/uldap.py", line 166, in _decorated
return func(self, _args,_ *kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/univention/uldap.py", line 650, in add
_rtype,_ rdata, _rmsgid, resp_ctrls = self.lo.add_ext_s(dn, nal, serverctrls=serverctrls)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 1016, in add_ext_s
return self._apply_method_s(SimpleLDAPObject.add_ext_s,*args,**kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 976, in _apply_method_s
return func(self,*args,**kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 221, in add_ext_s
msgid = self.add_ext(dn,modlist,serverctrls,clientctrls)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/ldap/ldapobject.py", line 218, in add_ext
return self._ldap_call(self._l.add_ext,dn,modlist,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 128, in _ldap_call
result = func(*args,**kwargs)
^^^^^^^^^^^^^^^^^^^^
TypeError: ('Tuple_to_LDAPMod(): expected a byte string in the list', 'Group for access to UDM REST API metrics')
22univention-directory-manager-rest.inst:
EXITCODE=1
Root Cause:
The failure was triggered by the introduction of Erratum 410 for UCS 5.2-5, which implemented a Prometheus-compatible metrics endpoint for the UDM REST API. This update required the automated creation of a specific group (udm-rest-metrics) via join scripts.
While the error message appeared during the creation of this group, the failure was not caused by the new software itself, but by a type mismatch introduced by a custom LDAP hook interacting with the updated Python environment in UCS 5.2.
The Technical Mechanism
The root cause is a conflict between how Python 3 handles strings (str) and what the underlying python-ldap library expects (bytes).
- Attribute Dependency: In the affected environment, a custom extended attribute named
DisplayName(mapping to the LDAP attributedisplayName) was configured. This attribute was set up via a hook (SetDisplayName) to automatically synchronize its value with thedescriptionfield whenever a group is created. - The Type Mismatch: In UCS 5.2, when the UDM module retrieves the
descriptionfield usingmodule.get('description'), it returns a Unicodestrobject. The custom hook appended thisstrdirectly to the LDAP attribute list (al) without encoding it intobytes. - The Fatal Error: When the
addoperation is executed in/usr/lib/python3/dist-packages/univention/uldap.py, thepython-ldaplibrary attempts to process the attribute list. Because the library’s underlying C implementation strictly expectsbytesfor LDAP values, it encounters the Unicode string and throws aTypeError.
Note on the Error Message:
The error message is often misleading:
TypeError: ('Tuple_to_LDAPMod(): expected a byte string in the list', 'Group for access to UDM REST API metrics')
The error text displays the content of the attribute (the description text) rather than the attribute name. Therefore, while it looks like the description is failing, the actual culprit is the displayName attribute being populated with a non-byte string.
Summary of Configuration Involved:
Custom Attribute Configuration:
name=DisplayName
DN: cn=DisplayName,cn=custom attributes,cn=univention,dc=univention,dc=de
CLIName: DisplayName
...
hook: SetDisplayName
ldapMapping: displayName
...
The Faulty Logic in the Hook:
# The value from 'description' is a 'str' in UCS 5.2
# Appending it directly causes the python-ldap crash
al.append(("displayName", None, module.get('description')))
Investigation
In my test environment, no DisplayName was created on the group. However, in the production environment, a hook was present that adds the extended attribute DisplayName upon group creation.
Test Environment (Working):
DN: cn=udm-rest-metrics,cn=groups,dc=miro,dc=intranet
adGroupType: -2147483646
createTimestamp: 2026-06-17 11:58:16
creatorsName: cn=admin,ca=miro,dc=intranet
description: Group for access to UDM REST API metrics
entryCSN: 20260617115816.716470Z#000000#000#000000
entryUUID: 91f75084-fe8f-1040-96a7-73968bf1489a
gidNumber: 5146
isOxGroup: OK
mailAddress: None
modifiersName: cn=admin,dc=miro,dc=intranet
modifyTimestamp: 2026-06-17 11:58:16
name: udm-rest-metrics
networkAccess: 0
nextcloudEnabled: 0
oxContext: None
sambaGroupType: 2
sambaRID: 11293
univentionObjectIdentifier: 91f75084-fe8f-1040-96a7-73968bf1489a
univentionObjectIdentifierView: ae57180e-f66d-4f7b-941f-1d4ba0ff5ad6
univentionSourceIAM: None
vlanId: None
root@ucs5primary:~# udm groups/group remove --dn cn="udm-rest-metrics,cn=groups,dc=miro,dc=intranet"
Object removed: cn=udm-rest-metrics,cn=groups,dc=miro,dc=intranet
root@ucs5primary:~# udm groups/group list --filter cn=udm-rest-metrics
cn=udm-rest-metrics
root@ucs5primary:~# univention-run-join-scripts --run-int scripts --force 22univention-directory-manager-rest.inst
univention-run-join-scripts: runs all join scripts existing on local computer.
copyright (c) 2001-2026 Univention GmbH, Germany
Running pre-joinscripts hook(s): done
Running 22univention-directory-manager-rest.inst done
Running post-joinscripts hook(s): done
univention-run-join-scripts started
Mi 17. Jun 13:58:06 CEST 2026
univention-join-hooks: looking for hook type "join/pre-joinscripts" on ucs5primary.miro.intranet
Found hooks:
cn=ucsschool-join-hook.py,cn=data,cn=univention,dc=miro,dc=intranet
Running: ucsschool-join-hook.py (cn=ucsschool-join-hook.py,cn=data,cn=univention,dc=miro,dc=intranet) in /tmp/tmpibdaqyzc/tmppqjg2kzn
2026-06-17 13:58:09,089 ucsschool-join-hook: [INFO] ucsschool-join-hook.py has been started
2026-06-17 13:58:09,091 ucsschool-join-hook: [INFO] Connecting to LDAP as 'cn=admin,dc=miro,dc=intranet' ...
2026-06-17 13:58:09,413 ucsschool-join-hook: [INFO] Determined role packages: []
2026-06-17 13:58:09,414 ucsschool-join-hook: [INFO] Calling ('/usr/bin/univention-app', 'info', '--as-json') ...
2026-06-17 13:58:11,978 ucsschool-join-hook: [INFO] Installed apps: ['cups=2.4.2', 'dhcp-umenter=16.0', 'ox-connector=3.2.0', 'provisioning-service=2.1', 'provisioning-service-backend=2.0', 'samba4=4.21', 'self-service=7.0', 'self-service-backend=7.0', 'ucsschool=5.2v6', 'ucsschool-id-connector=4.0.0', 'ucsschool-kelvin-rest-api=3.2.0', 'webweaver=4.0.0-3', '5.0/itslearning=5.0-ucs2', '5.0/keycloak=26.6.1-ucs1', '5.0/nextcloud=32.0.6-0']
2026-06-17 13:58:11,978 ucsschool-join-hook: [INFO] Is ucsschool already installed? True (5.2v6)
2026-06-17 13:58:11,978 ucsschool-join-hook: [INFO] Not installing 'UCS@school Veyon Proxy' app on this system role.
2026-06-17 13:58:11,981 ucsschool-join-hook: [INFO] ucsschool-join-hook.py is done
RUNNING 22univention-directory-manager-rest.inst
2026-06-17 13:58:12.427519471+02:00 (in joinscript_init)
Not updating directory/manager/rest/authorized-groups/domain-admins
Not updating directory/intranet/rest/authorized-groups/dc-backup
Not updating directory/manager/rest/authorized-groups/dc-slaves
Object exists: cn=services,cn=univention,dc=miro,dc=intranet
Object exists: cn=Univention Directory Manager REST,cn=services,cn=univention,dc=miro,dc=intranet
No modification: cn=ucs5primary,cn=dc,cn=computers,dc=miro,dc=intranet
WARNING: cannot append Univention Directory Manager REST to service, value exists
Object created: cn=udm-rest-metrics,cn=groups,dc=miro,dc=intranet
2026-06-17 13:58:16.804008972+02:00 (in joinscript_save_current_version)
EXITCODE=0
univention-join-hooks: looking for hook type "join/post-joinscripts" on ucs5primary.miro.intranet
Found hooks:
Mi 17. Jun 13:58:18 CEST 202int
univention-run-join-scripts finished
The Problem with the description Attribute
Interestingly, the group could be created without a description. As explained later, the explanation becomes clear when analyzing the error. In the custom hook SetDisplayName, the description is also supposed to be set. However, this value is passed as a Python str instead of bytes in the Attribute List (AL) for UDM.
I created the following group output:
udm groups/group create --position "cn=groups,$(ucr get ldap/base)" --set name="udm-rest-metrics"udm groups/group modify --dn cn="udm-rest-metrics,cn=groups,dc=univention,dc=de" --set description="Group of access to UDM REST API metrics"
The hook then sets the DisplayName attribute to None.
Production Environment:
root@x100:~# udm groups/group list --filter cn="udm-rest-metrics"
cn=udm-rest-metrics
DN: cn=udm-rest-metrics,cn=groups,dc=univention,dc=de
DisplayName: None
adGroupType: -2147483646
createTimestamp: 2026-06-17 09:53:51
description: Group of access to UDM REST API metrics
entryCSN: 20260617095659.297927Z#000000#000#000000
entryUUID: 303a5dca-fe7e-1040-8ad1-41f1385e2366
gidNumber: 4843
mailAddress: None
modifyTimestamp: 2026-06-17 09:56:59
name: udm-rest-metrics
networkAccess: 0
sambaGroupType: 2
sambaRID: 37046
univentionObjectIdentifier: e0104b3im-f62b-493b-a3ad-6d0c48e35165
univentionSourceIAM: None
vlanId: None
After this, the join script executed successfully.
Debugging uldap.py
In /usr/lib/python3/dist-packages/univention/uldap.py, there is an add operation section.
To identify the exact cause, I implemented a debug patch in uldap.py.
1. Backup existing file:
cp /usr/lib/python3/dist-packages/univention/uldap.py /root/uldap.py.bak
2. Edit and insert the following block (before line 648, before “try:”):
Note: Use 8 spaces for indentation to match the try: block level. Do not use Tabs.
# >>> DEBUG START
try:
_msg_lines = []
_msg_lines.append('========== DEBUG univention.uldap.add() ==========')
_msg_lines.append('DN: %r' % (dn,))
_msg_lines.append('al (raw input):')
for _entry in al:
_args_entry = _entry
_msg_lines.append(' %r' % (_args_entry,))
_msg_lines.append('nal (final, passed to add_ext_s):')
_items = nal.items() if isinstance(nal, dict) else nal
for _entry in _items:
_attr = _entry[0]
_val = _entry[-1]
_msg_lines.append(' attr=%r type=%s value=%r' % (_attr, type(_val).__name__, _val))
if isinstance(_val, (list, tuple)):
for _i, _item in enumerate(_val):
_msg_lines.append(' [%d] type=%s value=%r' % (_i, type(_item).__name__, _item))
if not isinstance(_item, bytes):
_msg_lines.append(' !!! NOT BYTES – likely culprit !!!')
else:
_msg_lines.append(' !!! NOT A LIST – likely culprit !!!')
_msg_lines.append('========== /DEBUG ==========')
_msg = '\n'.join(_msg_lines) + '\n'
import sys as _sys
try:
_sys.stderr.write(_msg)
_sys.stderr.flush()
except Exception:
pass
try:
with open('/tmp/uldap_add_debug.log', 'a') as _dbg:
_dbg.write(_msg)
except Exception:
pass
except Exception as _e:
try:
with open('/tmp/uldap_add_debug.log', 'a') as _dbg:
_dbg.write('DEBUG-FAIL: %r\n' % (_e,))
except Exception:
pass
# <<< DEBUG END
3. Syntax Check:
python3 -c "import py_compile; py_compile.compile('/usr/lib/python3/dist-packages/univention/uldap.py', doraise=True); print('OK')"
4. Clear Hook Pyc Cache and restart UDM CLI Server:
rm -f /usr/lib/python3/dist-packages/univention/admin/hooks.d/__pycache__/SetDisplayName*.pyc
pkill -f cli-server
5. Run Test with Debug Log generation:
univention-directory-manager groups/group create --position "cn=groups,$(ucr get ldap/base)" --set name="udm-rest-metrics" --set description="Group for access to UDM REST API metrics" 2>&1 | tee /tmp/udm-debug.log echo "" echo "=== /tmp/uldap_add_debug.log ==="
6. Analyze Resulting Log:
cat /tmp/uldap_add_debug.log
========== DEBUG univention.uldap.add() ==========
DN: 'cn=udm-rest-metrics,cn=groups,dc=univention,dc=de'
al (raw input):
('cn', b'', b'udm-rest-metrics')
('gidNumber', b'', b'4845')
('sambaGroupType', b'', b'2')
('univentionGroupType', b'', b'-2147483646')
('description', b'', b'Group for access to UDM REST API metrics')
('univentionObjectIdentifier', b'', b'41ceba8a-9446-46c4-9582-af52e77329fb')
('sambaSID', [b''], [b'S-1-4-4845'])
('objectClass', [b'top', b'posixGroup', b'sambaGroupMapping', b'univentionGroup'])
('displayName', None, 'Group for access to UDM REST API metrics')
('objectClass', [b'univentionObject'])
('univentionObjectType', [b'groups/group'])
nal (final, passed to add_ext_s):
attr='cn' type=list value=[b'udm-rest-metrics']
[0] type=bytes value=b'udm-rest-metrics'
attr='gidNumber' type=list value=[b'...'
...
attr='displayName' type=list value=['Group for access to UDM REST API metrics']
[0] type=str value='Group for access to UDM REST API metrics'
!!! NOT BYTES – likely culprit !!!
...
========== /DEBUG ==========
Explanation of the Issue
In the al (Attribute List), displayName is passed with an old value of None and a new value of 'Group for access to UDM REST API metrics' as a Python str.
Looking at the filter code in uldap.add() (~line 640):
if not val:
continue # <- None would be dropped here
if isinstance(val, bytes | str):
val = [val]
vals = nal.setdefault(key, set())
vals |= set(val)
The code accepts both bytes and str and wraps them in a list [val]. This results in a str being placed into the set. When add_ext_s is later called, python-ldap expects bytes and crashes with:
TypeError: ('Tuple_to_LDAPMod(): expected a byte string in the list', 'Group for access to UDM REST API metrics')
The error message identifies the value of the description because that is where the non-bytes string was detected—specifically, it is displayName, not description.
Why does displayName appear?
In Production (referencing the previous udm groups/group list output):
DisplayName: None
This means an Extended Attribute DisplayName (mapping to LDAP displayName) is registered for the groups/group UDM module in production, but not in my test environment. When a group is created, UDM automatically populates this property with the value from description. Because description is passed as a string, it enters the set as a string instead of bytes.
The Custom Hook causing the error:
root@x100:~# univention-ldapsearch -x -LLL -b "cn=custom attributes,cn=univention,$(ucr get ldap/base)" '(univentionUDMPropertyShortDescription=*)' univentionUDMPropertyShortDescription univentionUDMPropertyLdapMapping univentionUDMPropertyCLIName univentionUDMPropertyDefault univentionUDMPropertyHook univentionUDMPropertySyntax univentionUDMPropertyModule | grep -B1 -A8 -i display
dn: cn=DisplayName,cn=custom attributes,cn=univention,dc=univention,dc=de
univentionUDMPropertyModule: groups/group
univentionUDMPropertyLdapMapping: displayName
univentionUDMPropertySyntax: string
univentionUDMPropertyShortDescription: Display Name
univentionUDMPropertyCLIName: DisplayName
univentionUDMPropertyHook: SetDisplayName
dn: cn=ucs-monitoring,cn=custom attributes,cn=univention,dc=univention,dc=de
univentionUDMPropertyShortDescription: Assigned monitoring alerts
univentionUDMPropertySyntax: monitoringAlerts
univentionUDMPropertyHook: MonitoringComputer
univentionUDMPropertyLdapMapping: univentionDoesNotExists
univentionUDMPropertyModule: computers/domaincontroller_slave
univentionUDMPropertyModule: computers/domaincontroller_master
root@x100:~/univention-support# ls -lah /usr/lib/python3/dist-packages/univention/admin/hooks.d/
insgesamt 20K
drwxr-xr-x 3 root root 4,0K 29. Mai 12:46 .
drwxr-xr-x 8 root root 4,0K 29. Mai 12:46 ..
-rw-r--r-- 1 root root 0 18. Mai 16:57 __init__.py
-rw-r--r-- 1 root nogroup 1,9K 1. Jun 13:40 monitoring.py
drwxr-xr-x 2 root root 4,0K 1. Jun 13:40 __pycache__
-rw-r--r-- 1 root nogroup 1,5K 8. Mai 14:54 SetDisplayName.py
Original Hook Input (SetDisplayName.py):
from univention.admin.hook import simpleHook
class SetDisplayName(simpleHook):
type = "SetDisplayName"
def hook_ldap_addlist(self, module, al=[]):
if not module.get('DisplayName', None):
al.append(("displayName", None, module.get('description')))
return al
The root cause: module.get('description') returns a Python str in UCS 5.2. This value is appended to the al without verification. When add_ext_s() is called, python-ldap crashes because it requires bytes.
Solution
If you do not need this extended attribute or the hook, removing them will resolve the issue. In my case, I patched the hook.
1. Backup the hook:
cp /usr/lib/python3/dist-ast/univention/admin/hooks.d/SetDisplayName.py /root/SetDisplayName.py.bak
2. Edit the hook (/usr/lib/python3/dist-packages/univention/admin/hooks.d/SetDisplayName.py):
Update the logic to ensure encoding:
from univention.admin.hook import simpleHook
class SetDisplayName(simpleHook):
type = "SetDisplayName"
def hook_ldap_addlist(self, module, al=[]):
if module.get('DisplayName', None):
return al
value = module.get('description') or ''
if isinstance(value, str):
value = value.encode('UTF-8')
if value:
al.append(("displayName", b'', value))
return al
Key changes:
module.get('description')is converted tobytesbefore being appended.- The first element of the tuple is changed from
Nonetob''(the empty byte string), which is what UDM expects for the “old value” in Add operations and prevents potential issues withNone. - If
descriptionis empty, no entry is added.
3. Syntax Check and Cache Cleanup:
python3 -c "import py_compile; py_compile.compile('/usr/lib/python3/dist-packages/univention/admin/hooks.d/SetDisplayName.py', doraise=True); print('OK')"
rm -f /usr/lib/python3/dist-packages/univention/admin/hooks.d/__pycache__/SetDisplayName*.pyc
pkill -f cli-server
4. Revert Debug Patch in uldap.py (Important):
cp /root/uldap.py.bak /usr/lib/python3/dist-packages/univention/uldap.py
python3 -c "import py_compile; py_compile.compile('/usr/lib/python3/dist-packages/univention/uldap.py', doraise=True); print('OK')"
5. Verify the Fix:
Run the join script that triggers group creation:
univention-run-join-scripts --run-scripts --force 22univention-directory-manager-rest.inst
Output verification:
The group is created successfully, and displayName now correctly reflects the description as bytes.
Object created: cn=udm-rest-metrics,cn=groups,dc=univention,dc=de
...
root@x100:~# udm groups/group list --filter cn=udm-rest-metrics
cn=udm-rest-metrics
DN: cn=udm-rest-metrics,cn=groups,dc=univention,dc=de
DisplayName: Group for access to UDM REST API metrics
description: Group for access to UDM REST API metrics
...
Final Check of Join Status:
root@x100:~# univention-check-join-status
Joined successfully
References:
https://errata.software-univention.de/#/?erratum=5.2x410
https://forge.univention.org/bugzilla/show_bug.cgi?id=59176