7.8. UCS 5.0: Python 3 migration of modules and extensions#

UCS 5.0 switched from Python 2 to Python 3. This also affects Univention Directory Manager. Starting with UCS 5.0 the modules and extensions like syntax classes and hooks must be compatible with both Python versions to ensure easier transition.

Python 2 support will be removed completely with UCS 5.1.

This chapter describes important aspects of the migration as well as changes to the API.

7.8.1. Compatibility with UCS 4.4#

Most changes proposed in this chapter are compatible with UCS 4.4. One exception is the registration of the mapping encoding. The changes suggested here should already be included in the UDM modules for UCS 4.4 to make the update easier.

The changes suggested here should already be included for UCS 4.4. Otherwise, the update to UCS 5.0 may be problematic. Apps that still install UDM modules under UCS 4.4, while the Primary Directory Node may already be UCS 5, must also contain the customizations in the UDM modules or register 2 different variants, otherwise the app will not be displayed on the Primary Directory Node in Univention Management Console / Univention Directory Manager, for example.

For the registration of UDM extensions the parameters to specify the compatible starting and end UCS version are now mandatory. While a join script looked like:

Listing 7.6 Example for deprecated join script#
. /usr/share/univention-lib/ldap.sh

ucs_registerLDAPExtension "$@" \
    --udm_module /usr/lib/python3/dist-packages/univention/admin/handlers/foo/bar.py

it may now specify the compatible UCS versions:

Listing 7.7 Example for join script that defines the compatible UCS versions#
. /usr/share/univention-lib/ldap.sh

ucs_registerLDAPExtension "$@" \
    --ucsversionstart "4.4-0" \
    --ucsversionend "5.99-0" \
    --udm_module /usr/lib/python3/dist-packages/univention/admin/handlers/foo/bar.py

or register two separate versions compatible for each UCS version:

Listing 7.8 Example for join script that defines two UCS versions#
. /usr/share/univention-lib/ldap.sh

ucs_registerLDAPExtension "$@" \
    --ucsversionstart "4.4-0" \
    --ucsversionend "4.99-0" \
    --udm_module /usr/lib/python3/dist-packages/univention/admin/handlers/foo/bar.py

ucs_registerLDAPExtension "$@" \
    --ucsversionstart "5.0-0" \
    --ucsversionend "5.99-0" \
    --udm_module /usr/lib/python3/dist-packages/univention/admin/handlers/foo/bar.py

7.8.2. Default option#

If not already present, the module should define a default Univention Directory Manager option:

options = {
    'default': univention.admin.option(
        short_description=short_description,
        default=True,
        objectClasses=['top', 'objectClassName'],
    )
}
class object(...):
    ...

This enables generic functionality like automatic creation of search filters, automatic identification of objects and obsoletes the need to create the add-list manually.

7.8.3. Mapping functions#

The unmap functions must decode the given list of byte strings (bytes) into unicode strings (str). The map functions must encode the result of the unmap functions (for example unicode strings str) into a list of byte strings (bytes). Both functions have a new optional parameter encoding, which is a tuple consisting of the encoding (defaults to UTF-8) and the error handling in case de/encoding fails (defaults to strict).

Deprecated UCS 4 code most often looked like:

def map_function(value):
    return [value]


def unmap_function(value):
    return value[0]


mapping.register('property', 'attribute', map_function, unmap_function)

In UCS 5.0 the code has to look like:

def map_function(
    value: Union[Text, Sequence[Text]],
    encoding: Optional[Tuple[str, str]] = None,
) -> List[bytes]:
    return [value.encode(*encoding)]


def unmap_function(
    value: Sequence[bytes],
    encoding: Optional[Tuple[str, str]] = None,
) -> Text:
    return value[0].decode(*encoding)


mapping.register('property', 'attribute', map_function, unmap_function)

7.8.4. Mapping encoding#

Warning

Specifying the mapping encoding is incompatible with UCS 4.4.

The registration of the mapping of LDAP attributes to Univention Directory Manager properties now has to specify the correct encoding explicitly. The default encoding used is UTF-8. As most LDAP data is stored in UTF-8 the encoding parameter can be left out for most properties.

The encoding can simply be specified in the registration of a mapping:

mapping.register('property', 'attribute', map_function, unmap_function, encoding='ASCII')

The encoding depends on the LDAP syntax of the corresponding LDAP attribute. Syntaxes storing binary data should either be specified as ISO8859-1 or preferably should be decoded to an ASCII representation of base64 through univention.admin.mapping.mapBase64() and univention.admin.mapping.unmapBase64(). The attributes of the following syntaxes for example should be set to ASCII as they consist of ASCII only characters or a subset of ASCII (for example numbers).

  • IA5 String (1.3.6.1.4.1.1466.115.121.1.26)

  • Integer (1.3.6.1.4.1.1466.115.121.1.27)

  • Printable String (1.3.6.1.4.1.1466.115.121.1.44)

  • Boolean (1.3.6.1.4.1.1466.115.121.1.7)

  • Numeric String (1.3.6.1.4.1.1466.115.121.1.36)

  • Generalized Time (1.3.6.1.4.1.1466.115.121.1.24)

  • Telephone Number (1.3.6.1.4.1.1466.115.121.1.50)

  • UUID (1.3.6.1.1.16.1)

  • Authentication Password (1.3.6.1.4.1.4203.1.1.2)

To find out the syntax of an LDAP attribute programmatically for example for the attribute gecos:

python3 -c '
from univention.uldap import getMachineConnection
from ldap.schema import AttributeType
conn = getMachineConnection()
schema = conn.get_schema()
attr = schema.get_obj(AttributeType, "gecos")
print(atttr.syntax)'

7.8.5. object.open() / object._post_unmap()#

LDAP attributes contained in self.oldattr are usually transformed into property values (in self.info) by the mapping functions. In some cases this can’t be done automatically.

Instead this is done manually in the methods open() or _post_unmap(). These functions must consider transforming byte strings (bytes in self.oldattr) into unicode strings (str in self.info).

7.8.6. object.has_key()#

The method has_key() has been renamed into has_property(). The method has_property() is already present in UCS 4.4.

7.8.7. identify()#

The identify() function must now consider that the given attribute values are byte strings. The code prior looked like:

def identify(dn, attr, canonical=False):
    return 'objectClassName' in attr.get('objectClass', [])

In UCS 5.0 the code have to look like:

class object(...):
    ...
    @classmethod
    def identify(cls, dn, attr, canonical=False):
        return b'objectClassName' in attr.get('objectClass', [])


identify = object.identify

In most cases the identify() function only checks for the existence of a specific LDAP objectClass. The generic implementation can be used instead, which requires the default UDM option to be set:

options = {
    'default': univention.admin.option(
        short_description=short_description,
        default=True,
        objectClasses=['top', 'objectClassName'],
    )
}
class object(...):
    ...


identify = object.identify

7.8.8. _ldap_modlist()#

The methods _ldap_modlist() and _ldap_addlist() now must insert byte strings into the add/modlist. The code prior looked like:

class object(...):
    ...
    def _ldap_addlist(al):
        al = super(object, self)._ldap_addlist(al)
        al.append(('objectClass', ['top', 'objectClassName']))
        return al

    def _ldap_modlist(ml):
        ml = super(object, self)._ldap_modlist(ml)
        value = ...
        new = [value]
        ml.append(('attribute', self.oldattr.get('attribute', []), new))
        return ml

In UCS 5.0 the code have to look like:

class object(...):
    ...
    def _ldap_addlist(al):
        al = super(object, self)._ldap_addlist(al)
        al.append(('objectClass', [b'top', b'objectClassName']))
        return al

    def _ldap_modlist(ml):
        ml = super(object, self)._ldap_modlist(ml)
        value = ...
        new = [value.encode('UTF-8')]
        ml.append(('attribute', self.oldattr.get('attribute', []), new))
        return ml

The _ldap_addlist() is mostly not needed and should be replaced by specifying a default option (see above).

7.8.9. lookup()#

The lookup() should be replaced by specifying a default option as described above. The class method rewrite_filter() can be used to add additional filter rules.

7.8.10. Syntax classes#

Syntax classes now must ensure to return unicode strings.

7.8.11. Hooks#

For hooks the same rules as in _ldap_modlist() apply.