7.1. UDM modules#
Univention Directory Manager uses a flexible and extensible structure of Python modules to manage the directory service data. Additional modules are automatically recognized after being saved to the file system and made available for use at the command line and web interface. The development of custom modules enables the flexible extension of the Univention Directory Manager beyond the scope of extended attributes.
7.1.1. Overview#
Univention Directory Manager (UDM for short) uses its own module structure to map LDAP objects. Usually one of these UDM modules corresponds to an LDAP object (for example a user, a group or a container).
The modules are stored in the
/usr/lib/python3/dist-packages/univention/admin/handlers/
directory and
organized by task. The modules for managing the various computer objects are
located below the computers/
folder, for example. It can be addressed by
the command line interface through computers/windows
.
Custom modules should, if possible, be placed in their own subdirectory to avoid
conflicts with any standard modules that may later be integrated into UCS. For
the modules to be initialized, a __init__.py
file must exist in the
directory.
7.1.2. Structure of a module#
Modules contain the definition of the UDM properties and the definition
of a class named object
, which is derived from
univention.admin.handlers.simpleLdap
.
Note
The default name of the base class object
has historical reasons. It must
be kept despite the name collision with the
Python type object
.
This section will begin with a detailed description of the variables to be
defined. The The Python class object takes a closer look at the object
class and lists necessary definitions and functions within the class.
7.1.2.1. Global variables#
The global variables with specific meanings in a Univention Directory Manager module are described below. Mandatory and optional variables are separated into mandatory variables and optional arguments.
7.1.2.2. Mandatory variables#
- udm_modules_globals.module#
A string matching the name of the UDM module, for example
computers/computer
.
- udm_modules_globals.operations#
A list of strings which contains all LDAP operations allowed with this object. Available operations are
add
,edit
,remove
,search
,subtree_move
, andcopy
.
- udm_modules_globals.short_description#
This description is displayed as the name in the Univention Management Console. Within the UMC module LDAP navigation it is displayed in the selection list for possible object types.
- udm_modules_globals.long_description#
A detailed description of the module.
- udm_modules_globals.childs#
Indicates whether this LDAP object is a container. If so, this variable is set to the value
True
, and otherwise toFalse
.
- udm_modules_globals.options#
Variable
options
is a Python dictionary and defines various options that can either be set manually or left at default. These options can be changed later.For example through the web interface of the UDM using the Options tab. If an option is activated, one or more LDAP object classes (given by parameter
objectClass
) are added to the object and further fields and/or tabs are activated in the Univention Management Console tabs (for example the groupware option for users). The dictionary assigns a unique string to each option (asproperty_descriptions
).Each instance has the following parameters:
- options.short_description#
A short description of the option, used for example in the Univention Management Console as descriptive text about the input fields.
- options.long_description#
A longer description of the option.
- options.default#
defines whether the option is enabled by default:
True
means active andFalse
inactive.
- options.editable#
Defines whether this option can be set and removed multiple times, or always remains set after having been activated once.
- options.objectClasses#
A list of LDAP object classes, which the LDAP entry must consist of so that the option is enabled for the object.
Example:
options = { 'opt1': univention.admin.option( short_description=_('short description'), default=True, objectClasses=['class1'], ), }
- udm_modules_globals.property_descriptions#
This Python dictionary contains all UDM properties provided by the module. They are referenced using a unique string as a key (in this case as
univention.admin.property
objects). Usually, this kind of UDM property corresponds to an LDAP attribute, but can also be obtained or calculated from other sources.Example:
property_descriptions = { 'prop1': univention.admin.property( short_description=_('name'), long_description=_('long description'), syntax=univention.admin.syntax.string, multivalue=False, required=True, may_change=True, identifies=False, dontsearch=True, default=('default value'), options=['opt1'], ), }
A short explanation of the parameters seen above:
- property_descriptions.short_description: str#
A short description used for instance in the Univention Management Console as descriptive text to the input fields.
- property_descriptions.long_description: str#
A detailed description used in the Univention Management Console for the tooltips.
- property_descriptions.syntax: type#
This parameter specifies the property type. Based on these type definitions, the Univention Directory Manager can check the specified values for the property and provide a detailed error message in case of invalid values. A list of syntax classes is available in UDM LDAP search.
- property_descriptions.multivalue: bool#
Accepts the values
True
orFalse
. If set toTrue
the properties value is a list. In this case, the syntax parameter specifies the type of elements within this list.
- property_descriptions.required: bool#
If this parameter is set to
True
, a value must be specified for this property.
- property_descriptions.may_change: bool#
If set to
True
, the properties value can be modified at a later point, if not, it can only be specified once when the object is created.
- property_descriptions.editable: bool#
If set to
False
, the properties value can’t even be specified when the object is created. This is usually only interesting or useful for automatically generated or calculated values.
- property_descriptions.identifies: bool#
This option should be set to
True
if the property uniquely identifies the object (through the LDAP DN). In most cases it should be set for exactly one property of a module.
- property_descriptions.default: Any#
The default value of a property, when the object is created through the Univention Management Console.
- udm_modules_globals.layout#
The UDM properties of an object can be arranged in groups. They are represented as tabs in the Univention Directory Manager for example. For each tab, an instance of
univention.admin.layout#Tab
must be created in the arraylayout
. The name, a description for the tab and a list of rows are expected as parameters. A line can contain up to two properties, for each of which an instance ofunivention.admin.layout#Group
must be created. The UDM property name fromproperty_descriptions
is expected as a parameter for each instance.from univention.admin.layout import Tab, Group layout = [ Tab(_('Tab header'), _('Tab description'), layout=[ Group('Group', 'group description', [ ['prop1', 'prop2'] ['prop3', ] ]), ... ], advanced=True), ... ]
The optional
advanced=True
setting controls whether the tab should be displayed on the Advanced settings by default.
- udm_modules_globals.mapping#
Maps the UDM properties to LDAP attributes. Usually, a mapping is registered for each property, linking the name of a UDM property (
udm_name
) to the associated LDAP attribute (ldap_name
):mapping.register(udm_name, ldap_name) mapping.register(udm_name, ldap_name, map_value, unmap_value)
Two functions are available to convert the values between UDM properties and LDAP attribute. To convert from UDM → LDAP,
map_value()
is used, whileunmap_value()
is used to convert in the opposite direction (LDAP → UDM). The second function is necessary for all single-valued UDM properties, since these are always implemented as null or one-element lists within LDAP. The default implementationunivention.admin.mapping.ListToString()
always returns the first entry of the list and can therefore generally be specified as aunmap_value()
function for all single-valued attributes. Formap_value()
(UDM → LDAP), it is sufficient to specifyNone
, which ensures that any existing value, if present, is converted to a single-element list.Warning
UDM properties always contain either a string (single-valued attributes) or a list of strings (multi-valued attributes), never just a number or any other Python type!
7.1.2.3. Optional arguments#
The following specifications are optional and only need to be defined if a module has these special properties:
- udm_modules_globals.virtual#
Modules that set this variable to
True
are a kind of helper module for other modules that have no associated LDAP objects. An example of this is thecomputers/computer
module, which is an auxiliary module for all types of computers.
- udm_modules_globals.template#
A module that sets this variable to another UDM module (e.g.
settings/usertemplate
), gains the ability to define default values for UDM properties from other modules. An example of this is the user template (more specifically thesettings/usertemplate
module). Such a template can for example be selected when creating a user so that the values defined in it are taken over as defaults in the input masks.
7.1.2.4. The Python class object
#
The Python class object
of a module provides the interface between
Univention Directory Manager and the LDAP operations triggered when an object is created,
modified, moved or deleted. It supports the Univention Directory Manager in mapping the UDM
module and its properties to LDAP objects and attributes.
This requires adhering to the predefined API of the class. The base
class univention.admin.handlers.simpleLdap
provides the essential
functionality for simple LDAP objects, so usually only a few adjustments
are necessary. An instance (self
) encapsulates all information of an
object, which can be accessed in various ways:
- class udm_modules_globals.object#
self.dn
→ StringDistinguished Name in the LDAP DIT
self.position
→univention.admin.uldap#Position
Container element in the LDAP DIT
self['UDM-property-name']
→ [values, …]Wrapper around
self.info
which also checks the value against the syntax when assigned and returns default values when read.self.info['UDM-property-name']
→ [values, …]Dictionary with the currently set values of the UDM properties. Direct access to it allows the initialization of
editable=False
properties and skips any syntax checks.self.oldinfo['UDM-property-name']
→ [values, …]Dictionary of the originally read values converted to UDM property names. It is primarily needed to internally propagate changes to the Python object back to the corresponding entry in the LDAP.
self.oldattr['LDAP-Attributname']
→ [values, …]Dictionary of the attributes originally read from LDAP.
self.oldpolicies
→ [Policy-DNs
, …]Copy of the list of DNs of the referenced
univentionPolicyReference
self.policies
→ [Policy-DNs
, …]List of DNs of the referenced
univentionPolicyReference
self.policyObjects[Policy-DN]
→univention.admin.handlers#SimplePolicy
Dictionary of the loaded policies.
self.extended_udm_attributes
→ [univention.admin#Extended_attribute
, …]Complete list of the objects
extended attributes
The simpleLdap
class also provides the possibility of additional
customization before and after the LDAP operation by calling functions. For
example, before creating an LDAP object the function
_ldap_pre_create()
is called and after the operation the function
_ldap_post_create()
is called. Such pre- and post-functions similarly
exist for the modify()
, move()
and remove()
functions. The following table lists all used functions in calling order from
top to bottom:
Description |
Create |
Modify |
Remove |
---|---|---|---|
Before validation |
|
||
Validates, that all required attributes are set |
|
||
|
|
|
|
Policy Copy-on-Write |
|
|
|
Extension point for Extended Attribute |
|
|
|
Returns initial list of (LDAP-attribute-name, value)- resp. (LDAP-attribute-name, [values]) tuples |
|
||
Calculates difference between |
|
||
Extension point for Extended Attribute |
|
|
|
Real action |
ADD |
MODIFY |
DELETE |
|
|
|
|
Extension point for Extended Attribute |
|
|
|
The functions hook_ldap_*
are described in Extended attribute hooks.
7.1.2.5. The identify()
and lookup()
functions#
These functions are used to find the corresponding objects for search queries
from the Univention Management Console (lookup()
) and to assign LDAP objects to a Univention Directory Manager
module. For simple LDAP objects, no modifications are necessary. They can be
assigned to the generic objects
class methods:
lookup = object.lookup
lookup_filter = object.lookup_filter
identify = object.identify
7.1.3. Example module#
The following is an example module for the Univention Directory Manager which is also available as a package. (univention-directory-manager-module-example) The complete source code is available at UCS source: packaging/univention-directory-manager-module-example/.
The directory contains a source package in Debian format, from which two binary packages are created during package build through ./debian/rules binary: A schema package, which must be installed on the Primary Directory Node, and the package containing the UDM module itself. The sample code also includes a ip-phone-tool script that shows an example of using the UDM Python API in a Python script.
A Univention Directory Manager module almost always consists of two components:
The Python module, which contains the implementation of the interface to the Univention Directory Manager.
A LDAP schema, which defines the LDAP object to be managed. Both parts are described below, with the focus lying on the creation of the Python module.
The following module for the Univention Directory Manager demonstrates the rudimentary administration of IP telephones. It tries to show as many possibilities of a Univention Directory Manager module as possible within a simple example.
7.1.3.1. Python code of the example module#
Before defining the actual module source code, some basic Python modules need to be imported, which are always necessary:
import re
import univention.admin.handlers
import univention.admin.syntax
import univention.admin.localization
from univention.admin.layout import Tab
This list of Python modules can of course be extended. As described in Global variables, some necessary global variables are defined at the beginning of a Univention Directory Manager module, which provide a description of the module:
module = 'test/ip_phone'
childs = False
short_description = _('IP-Phone')
long_description = _('An example module for the Univention Directory Manager')
operations = ['add', 'edit', 'remove', 'search', 'move', 'copy']
Another global variable important for the Univention Management Console, is
layout
.
layout = [
Tab(_('General'), _('Basic Settings'), layout=[
["name", "active"],
["ip", "protocol"],
["priuser"],
]),
Tab(_('Advanced'), _('Advanced Settings'), layout=[
["users"],
], advanced=True),
Tab(_('Redirect'), _('Redirect Option'), layout=[
["redirect_user"],
], advanced=True),
]
It structures the layout of the objects individual properties on the tabs. The
list consists of elements whose type is univention.admin.layout.Tab
,
each determining the content of a tab. In this case there are the General
,
Advanced
and Redirect
tabs. Next, the options (options
) and properties (property_descriptions
) of the module should be defined.
In this case, the default
and redirection
options are created, whose
functions will be explained later. To configure the parameters, the
univention.admin.option
object is passed to the
short_description
option for a short description. default
defines the
pre-configuration. True
activates the option while False
deactivates it.
options = {
'default': univention.admin.option(
short_description=short_description,
default=True,
objectClasses=['top', 'testPhone'],
),
'redirection': univention.admin.option(
short_description=_('Call redirect option'),
default=True,
editable=True,
objectClasses=['testPhoneCallRedirect'],
),
}
After the modules options, its properties are defined. UDM properties are defined through textual descriptions, syntax definitions and instructions for the Univention Management Console.
property_descriptions = {
...
}
The name
property defines the hostname
of the IP phone. The syntax
parameter tells the Univention Directory Manager that valid values for this property must match the
syntax of a computer name. Additional predefined syntax definitions can be found
in the property_descriptions
section.
'name': univention.admin.property(
short_description=_('Name'),
long_description=_('ID of the IP-phone'),
syntax=univention.admin.syntax.hostName,
required=True,
identifies=True,
),
The active
is an example of a boolean/binary property which can only take
the values True
or False
. In this example, it defines an
activation/blocking of the IP phone. The parameter default=True
initially
unlocks the phone:
'active': univention.admin.property(
short_description=_('active'),
long_description=_('The IP-phone can be deactivated'),
syntax=univention.admin.syntax.TrueFalseUp,
default='TRUE',
),
The protocol
property specifies which VoIP protocol is supported by the
phone. No standard syntax definition is used for this property, but a specially
declared SynVoIP_Protocols
class. (The source code of this class follows in
a later section). The syntax of the class defines a selection list with a
predefined set of possibilities. The default
parameter preselects the value
with the sip
key.
'protocol': univention.admin.property(
short_description=_('Protocol'),
long_description=_('Supported VoIP protocols'),
syntax=SynVoIP_Protocols
default='sip',
),
The ip
property specifies the phones IP address. The predefined class
univention.admin.syntax.ipAddress
is specified as the syntax
definition. Additionally, the required
parameter enforces that setting this
property is mandatory.
'ip': univention.admin.property(
short_description=_('IP-Address'),
long_description=_('IP-Address of the IP-phone'),
syntax=univention.admin.syntax.ipAddress,
required=True,
),
The priuser
property sets the primary user of the IP phone. A separate
syntax definition is again used, which in this case is a class that defines the
valid values by means of a regular expression. (The source code is shown later)
'priuser': univention.admin.property(
short_description=_('Primary User'),
long_description=_('The primary user of this IP-phone'),
syntax=SynVoIP_Address,
required=True,
),
The users
property indicates that options are used. Since multivalue
is
set to True
in this example, the users
object is a list of addresses.
'users': univention.admin.property(
short_description=_('Additional Users'),
long_description=_('Users, that may register with this phone'),
syntax=SynVoIP_Address,
multivalue=True,
),
The redirect_user
property is used to redirect incoming calls to a different
phone number. It is only shown if the options=['redirection']
is set.
'redirect_user': univention.admin.property(
short_description=_('Redirection User'),
long_description=_('Address for call redirection'),
syntax=SynVoIP_Address,
options=['redirection'],
),
The following two classes are the syntax definitions used for the protocols
,
priuser
and users
properties. SynVoIP_Protocols
is based on the
predefined univention.admin.syntax.select
class, which provides the basic
functionality for select lists. Derived classes, as seen in the following class,
only need to define a name and the list of choices.
class SynVoIP_Protocols(univention.admin.syntax.select):
name = _('VoIP_Protocol')
choices = [('sip', _('SIP')), ('h323', _('H.323')), ('skype', _('Skype'))]
The other syntax definition (SynVoIP_Address
) is based on the
univention.admin.syntax.simple
class, which provides basic
functionality for syntax definitions utilizing regular expressions. As with the
other definition, a name must be assigned. Additionally, the attributes
min_length
and max_length
must be specified. If one of these attributes
is set to 0
, it corresponds to a nonexistent limit in the respective
direction. In addition to the attributes mentioned, the parse()
function must also be defined, which passes the value to be checked as a
parameter. By means of the Python module re it is in this case
checked whether the value corresponds to the pattern of a VoIP address, e.g.
sip:hans@mustermann.de
.
class SynVoIP_Address(univention.admin.syntax.simple):
name = _('VoIP_Address')
min_length = 4
max_length = 256
_re = re.compile('((^(sip|h323|skype):)?([a-zA-Z])[a-zA-Z0-9._-]+)@[a-zA-Z0-9._-]+$')
def parse(self, text):
if self._re.match(text) is not None:
return text
raise univention.admin.uexceptions.valueError(_('Not a valid VoIP Address'))
Mapping the UDM module properties to the Attributes of the to be created LDAP
object is the next step. (mapping
). To
do this, the univention.admin.mapping.mapping
class is used, which
provides a simple way to register mappings for the individual LDAP attributes to
UDM properties with the register()
function. This function’s first
argument is the modules UDM property name and the second the LDAP attribute
name. The following two arguments of the register()
function can be
used to specify mapping functions for conversion from the modules UDM property
to the LDAP attribute and vice versa.
mapping = univention.admin.mapping.mapping()
mapping.register('name', 'cn', None, univention.admin.mapping.ListToString)
mapping.register('active', 'testPhoneActive', None, univention.admin.mapping.ListToString)
mapping.register('protocol', 'testPhoneProtocol', None, univention.admin.mapping.ListToString)
mapping.register('ip', 'testPhoneIP', None, univention.admin.mapping.ListToString)
mapping.register('priuser', 'testPhonePrimaryUser', None, univention.admin.mapping.ListToString)
mapping.register('users', 'testPhoneUsers')
mapping.register('redirect_user', 'testPhoneRedirectUser', None, univention.admin.mapping.ListToString)
Finally, The Python class object must be defined for the module that conforms to the specifications defined in Structure of a module. For the IP phone, the class would look like this:
class object(univention.admin.handlers.simpleLdap):
module = module
def open(self):
super(object, self).open()
self.save()
def _ldap_pre_create(self):
return super(object, self)._ldap_pre_create()
def _ldap_post_create(self):
return super(object, self)._ldap_post_create()
def _ldap_pre_modify(self):
return super(object, self)._ldap_pre_modify()
def _ldap_post_modify(self):
return super(object, self)._ldap_post_modify()
def _ldap_pre_remove(self):
return super(object, self)._ldap_pre_remove()
def _ldap_post_remove(self):
return super(object, self)._ldap_post_remove()
def _ldap_modlist(self):
ml = super(object, self)._ldap_modlist()
return ml
To enable searching for objects managed by this module, two additional functions
are available: lookup()
and identify()
(see The identify() and lookup() functions). The functions provided here should be sufficient
for simple LDAP objects that can be identified by a single objectClass
.
lookup = object.lookup
lookup_filter = object.lookup_filter
identify = object.identify
7.1.3.2. LDAP schema extension for the example module#
Before the developed module can be used within the Univention Directory Manager, the new
object class, in this case testPhone
, must be made known to the LDAP
server together with its attributes. Such object definitions are defined
via so-called schemas in LDAP. They are specified in files looking like
the following:
attributetype ( 1.3.6.1.4.1.10176.9999.1.1 NAME 'testPhoneActive'
DESC 'state of the IP phone'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.10176.9999.1.2 NAME 'testPhoneProtocol'
DESC 'The supported VoIP protocol'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.10176.9999.1.3 NAME 'testPhoneIP'
DESC 'The IP address of the phone'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.10176.9999.1.4 NAME 'testPhonePrimaryUser'
DESC 'The primary user of the phone'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.10176.9999.1.5 NAME 'testPhoneUsers'
DESC 'A list of other users allowed to use the phone'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
objectclass ( 1.3.6.1.4.1.10176.9999.2.1 NAME 'testPhone'
DESC 'IP Phone'
SUP top STRUCTURAL
MUST ( cn $ testPhoneActive $ testPhoneProtocol $ testPhoneIP $ testPhonePrimaryUser )
MAY ( testPhoneUsers )
)
Detailed documentation on creating LDAP schema files can be found on the OpenLDAP project website and is not the focus of this documentation.
7.1.3.3. Installing the module#
The last step is to install the Python module and LDAP schema, documented in the following.
The Python module must be copied to the
/usr/lib/python2.7/dist-packages/univention/admin/handlers/
and
/usr/lib/python3/dist-packages/univention/admin/handlers/
directory for
the Univention Directory Manager to find it. In this directory a subdirectory has to be created
corresponding to the first part of the module name. For example, if the module
name is test/ip-phone
, the directory should be named test/
. The
Python module must then be copied to this directory. Ideally, a UDM module is
integrated into a separate Debian package.
Documentation for this can be found in the Introduction section. The newly created package will now be included in the display when univention-directory-manager modules is called.
In principle, the file containing the LDAP schema can be copied to any
directory. Univention schema definitions, for example, are stored in the
/usr/share/univention-ldap/schema/
directory. For the LDAP server to
find this schema, it must be included in the /etc/ldap/slapd.conf
configuration file. Since this file is under the control of the Univention
Configuration Registry, do not edit the file directly, but create a Univention
Configuration Registry template. (see UCR Template files conffiles/path/to/file)
7.1.3.4. Downloading the sample code#
The latest version of the sample code can be found at UCS source: packaging/univention-directory-manager-module-example/.
It contains a source package in Debian format from which two binary packages are created during package building through ./debian/rules binary: A schema package that needs to be installed on the master and the package containing the UDM module itself. The sample code also includes a script ip-phone-tool, which exemplifies the use of the UDM Python API in a Python script.