Problem: UCS 5.2-5 - Join Status Error Caused by Failed Creation of the udm-rest-metrics

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).

  1. Attribute Dependency: In the affected environment, a custom extended attribute named DisplayName (mapping to the LDAP attribute displayName) was configured. This attribute was set up via a hook (SetDisplayName) to automatically synchronize its value with the description field whenever a group is created.
  2. The Type Mismatch: In UCS 5.2, when the UDM module retrieves the description field using module.get('description'), it returns a Unicode str object. The custom hook appended this str directly to the LDAP attribute list (al) without encoding it into bytes.
  3. The Fatal Error: When the add operation is executed in /usr/lib/python3/dist-packages/univention/uldap.py, the python-ldap library attempts to process the attribute list. Because the library’s underlying C implementation strictly expects bytes for LDAP values, it encounters the Unicode string and throws a TypeError.

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 to bytes before being appended.
  • The first element of the tuple is changed from None to b'' (the empty byte string), which is what UDM expects for the “old value” in Add operations and prevents potential issues with None.
  • If description is 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