From b189e66298816c3414e027c914b5e62f30512330 Mon Sep 17 00:00:00 2001 From: Petr Vobornik Date: Jun 04 2015 10:06:31 +0000 Subject: topology: ipa management commands ipalib part of topology management Design: - http://www.freeipa.org/page/V4/Manage_replication_topology https://fedorahosted.org/freeipa/ticket/4302 Reviewed-By: Martin Babinsky --- diff --git a/API.txt b/API.txt index 7574bc9..c47d800 100644 --- a/API.txt +++ b/API.txt @@ -4560,6 +4560,161 @@ option: Str('version?', exclude='webui') output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) output: Output('summary', (, ), None) output: PrimaryKey('value', None, None) +command: topologysegment_add +args: 2,13,3 +arg: Str('topologysuffixcn', cli_name='topologysuffix', multivalue=False, primary_key=True, query=True, required=True) +arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, primary_key=True, required=True) +option: Str('addattr*', cli_name='addattr', exclude='webui') +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: StrEnum('iparepltoposegmentdirection', attribute=True, cli_name='direction', default=u'both', multivalue=False, required=True, values=(u'both', u'left-right', u'right-left', u'none')) +option: Str('iparepltoposegmentleftnode', attribute=True, cli_name='leftnode', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9.][a-zA-Z0-9.-]{0,252}[a-zA-Z0-9.$-]?$', required=True) +option: Str('iparepltoposegmentrightnode', attribute=True, cli_name='rightnode', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9.][a-zA-Z0-9.-]{0,252}[a-zA-Z0-9.$-]?$', required=True) +option: StrEnum('nsds5replicaenabled', attribute=True, cli_name='enabled', multivalue=False, required=False, values=(u'on', u'off')) +option: Str('nsds5replicastripattrs', attribute=True, cli_name='stripattrs', multivalue=False, required=False) +option: Str('nsds5replicatedattributelist', attribute=True, cli_name='replattrs', multivalue=False, required=False) +option: Str('nsds5replicatedattributelisttotal', attribute=True, cli_name='replattrstotal', multivalue=False, required=False) +option: Int('nsds5replicatimeout', attribute=True, cli_name='timeout', minvalue=0, multivalue=False, required=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Str('setattr*', cli_name='setattr', exclude='webui') +option: Str('version?', exclude='webui') +output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: PrimaryKey('value', None, None) +command: topologysegment_del +args: 2,2,3 +arg: Str('topologysuffixcn', cli_name='topologysuffix', multivalue=False, primary_key=True, query=True, required=True) +arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=True, primary_key=True, query=True, required=True) +option: Flag('continue', autofill=True, cli_name='continue', default=False) +option: Str('version?', exclude='webui') +output: Output('result', , None) +output: Output('summary', (, ), None) +output: ListOfPrimaryKeys('value', None, None) +command: topologysegment_find +args: 2,15,4 +arg: Str('topologysuffixcn', cli_name='topologysuffix', multivalue=False, primary_key=True, query=True, required=True) +arg: Str('criteria?', noextrawhitespace=False) +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: Str('cn', attribute=True, autofill=False, cli_name='name', maxlength=255, multivalue=False, primary_key=True, query=True, required=False) +option: StrEnum('iparepltoposegmentdirection', attribute=True, autofill=False, cli_name='direction', default=u'both', multivalue=False, query=True, required=False, values=(u'both', u'left-right', u'right-left', u'none')) +option: Str('iparepltoposegmentleftnode', attribute=True, autofill=False, cli_name='leftnode', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9.][a-zA-Z0-9.-]{0,252}[a-zA-Z0-9.$-]?$', query=True, required=False) +option: Str('iparepltoposegmentrightnode', attribute=True, autofill=False, cli_name='rightnode', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9.][a-zA-Z0-9.-]{0,252}[a-zA-Z0-9.$-]?$', query=True, required=False) +option: StrEnum('nsds5replicaenabled', attribute=True, autofill=False, cli_name='enabled', multivalue=False, query=True, required=False, values=(u'on', u'off')) +option: Str('nsds5replicastripattrs', attribute=True, autofill=False, cli_name='stripattrs', multivalue=False, query=True, required=False) +option: Str('nsds5replicatedattributelist', attribute=True, autofill=False, cli_name='replattrs', multivalue=False, query=True, required=False) +option: Str('nsds5replicatedattributelisttotal', attribute=True, autofill=False, cli_name='replattrstotal', multivalue=False, query=True, required=False) +option: Int('nsds5replicatimeout', attribute=True, autofill=False, cli_name='timeout', minvalue=0, multivalue=False, query=True, required=False) +option: Flag('pkey_only?', autofill=True, default=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Int('sizelimit?', autofill=False, minvalue=0) +option: Int('timelimit?', autofill=False, minvalue=0) +option: Str('version?', exclude='webui') +output: Output('count', , None) +output: ListOfEntries('result', (, ), Gettext('A list of LDAP entries', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: Output('truncated', , None) +command: topologysegment_mod +args: 2,15,3 +arg: Str('topologysuffixcn', cli_name='topologysuffix', multivalue=False, primary_key=True, query=True, required=True) +arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, primary_key=True, query=True, required=True) +option: Str('addattr*', cli_name='addattr', exclude='webui') +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: Str('delattr*', cli_name='delattr', exclude='webui') +option: StrEnum('iparepltoposegmentdirection', attribute=True, autofill=False, cli_name='direction', default=u'both', multivalue=False, required=False, values=(u'both', u'left-right', u'right-left', u'none')) +option: Str('iparepltoposegmentleftnode', attribute=True, autofill=False, cli_name='leftnode', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9.][a-zA-Z0-9.-]{0,252}[a-zA-Z0-9.$-]?$', required=False) +option: Str('iparepltoposegmentrightnode', attribute=True, autofill=False, cli_name='rightnode', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9.][a-zA-Z0-9.-]{0,252}[a-zA-Z0-9.$-]?$', required=False) +option: StrEnum('nsds5replicaenabled', attribute=True, autofill=False, cli_name='enabled', multivalue=False, required=False, values=(u'on', u'off')) +option: Str('nsds5replicastripattrs', attribute=True, autofill=False, cli_name='stripattrs', multivalue=False, required=False) +option: Str('nsds5replicatedattributelist', attribute=True, autofill=False, cli_name='replattrs', multivalue=False, required=False) +option: Str('nsds5replicatedattributelisttotal', attribute=True, autofill=False, cli_name='replattrstotal', multivalue=False, required=False) +option: Int('nsds5replicatimeout', attribute=True, autofill=False, cli_name='timeout', minvalue=0, multivalue=False, required=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Flag('rights', autofill=True, default=False) +option: Str('setattr*', cli_name='setattr', exclude='webui') +option: Str('version?', exclude='webui') +output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: PrimaryKey('value', None, None) +command: topologysegment_refresh +args: 2,4,3 +arg: Str('topologysuffixcn', cli_name='topologysuffix', multivalue=False, primary_key=True, query=True, required=True) +arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, primary_key=True, query=True, required=True) +option: Flag('left?', autofill=True, default=False) +option: Flag('right?', autofill=True, default=False) +option: Flag('stop?', autofill=True, default=False) +option: Str('version?', exclude='webui') +output: Output('result', , None) +output: Output('summary', (, ), None) +output: PrimaryKey('value', None, None) +command: topologysegment_show +args: 2,4,3 +arg: Str('topologysuffixcn', cli_name='topologysuffix', multivalue=False, primary_key=True, query=True, required=True) +arg: Str('cn', attribute=True, cli_name='name', maxlength=255, multivalue=False, primary_key=True, query=True, required=True) +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Flag('rights', autofill=True, default=False) +option: Str('version?', exclude='webui') +output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: PrimaryKey('value', None, None) +command: topologysuffix_add +args: 1,6,3 +arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, required=True) +option: Str('addattr*', cli_name='addattr', exclude='webui') +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: Str('iparepltopoconfroot', attribute=True, cli_name='suffix', maxlength=255, multivalue=False, required=True) +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Str('setattr*', cli_name='setattr', exclude='webui') +option: Str('version?', exclude='webui') +output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: PrimaryKey('value', None, None) +command: topologysuffix_del +args: 1,2,3 +arg: Str('cn', attribute=True, cli_name='name', multivalue=True, primary_key=True, query=True, required=True) +option: Flag('continue', autofill=True, cli_name='continue', default=False) +option: Str('version?', exclude='webui') +output: Output('result', , None) +output: Output('summary', (, ), None) +output: ListOfPrimaryKeys('value', None, None) +command: topologysuffix_find +args: 1,8,4 +arg: Str('criteria?', noextrawhitespace=False) +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: Str('cn', attribute=True, autofill=False, cli_name='name', multivalue=False, primary_key=True, query=True, required=False) +option: Str('iparepltopoconfroot', attribute=True, autofill=False, cli_name='suffix', maxlength=255, multivalue=False, query=True, required=False) +option: Flag('pkey_only?', autofill=True, default=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Int('sizelimit?', autofill=False, minvalue=0) +option: Int('timelimit?', autofill=False, minvalue=0) +option: Str('version?', exclude='webui') +output: Output('count', , None) +output: ListOfEntries('result', (, ), Gettext('A list of LDAP entries', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: Output('truncated', , None) +command: topologysuffix_mod +args: 1,8,3 +arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True) +option: Str('addattr*', cli_name='addattr', exclude='webui') +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: Str('delattr*', cli_name='delattr', exclude='webui') +option: Str('iparepltopoconfroot', attribute=True, autofill=False, cli_name='suffix', maxlength=255, multivalue=False, required=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Flag('rights', autofill=True, default=False) +option: Str('setattr*', cli_name='setattr', exclude='webui') +option: Str('version?', exclude='webui') +output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: PrimaryKey('value', None, None) +command: topologysuffix_show +args: 1,4,3 +arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True) +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Flag('rights', autofill=True, default=False) +option: Str('version?', exclude='webui') +output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: PrimaryKey('value', None, None) command: trust_add args: 1,13,3 arg: Str('cn', attribute=True, cli_name='realm', multivalue=False, primary_key=True, required=True) diff --git a/VERSION b/VERSION index 2ad3827..6f6e363 100644 --- a/VERSION +++ b/VERSION @@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000 # # ######################################################## IPA_API_VERSION_MAJOR=2 -IPA_API_VERSION_MINOR=123 -# Last change: rcritten - added service constraint delegation plugin +IPA_API_VERSION_MINOR=124 +# Last change: pvoborni - added topology management commands diff --git a/ipalib/constants.py b/ipalib/constants.py index 96396a2..93d7aaa 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -119,6 +119,7 @@ DEFAULT_CONFIG = ( ('container_views', DN(('cn', 'views'), ('cn', 'accounts'))), ('container_masters', DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'))), ('container_certprofile', DN(('cn', 'certprofiles'), ('cn', 'ca'))), + ('container_topology', DN(('cn', 'topology'), ('cn', 'ipa'), ('cn', 'etc'))), # Ports, hosts, and URIs: ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'), diff --git a/ipalib/plugins/topology.py b/ipalib/plugins/topology.py new file mode 100644 index 0000000..ba99133 --- /dev/null +++ b/ipalib/plugins/topology.py @@ -0,0 +1,385 @@ +# +# Copyright (C) 2015 FreeIPA Contributors see COPYING for license +# + +from ipalib import api, errors +from ipalib import Int, Str, Bool, StrEnum, Flag +from ipalib.plugable import Registry +from ipalib.plugins.baseldap import ( + LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete, LDAPUpdate, LDAPQuery, + LDAPRetrieve) +from ipalib import _, ngettext +from ipalib import output +from ipapython.dn import DN + + +__doc__ = _(""" +Topology + +Management of a replication topology. + +Requires minimum domain level 1. +""") + +register = Registry() + +MINIMUM_DOMAIN_LEVEL = 1 + + +def validate_domain_level(api): + current = int(api.Command.domainlevel_get()['result']) + if current < MINIMUM_DOMAIN_LEVEL: + raise errors.InvalidDomainLevelError( + _('Topology management requires minimum domain level {0} ' + .format(MINIMUM_DOMAIN_LEVEL)) + ) + + +@register() +class topologysegment(LDAPObject): + """ + Topology segment. + """ + parent_object = 'topologysuffix' + container_dn = api.env.container_topology + object_name = _('segment') + object_name_plural = _('segments') + object_class = ['iparepltoposegment'] + default_attributes = [ + 'cn', + 'ipaReplTopoSegmentdirection', 'ipaReplTopoSegmentrightNode', + 'ipaReplTopoSegmentLeftNode', 'nsds5replicastripattrs', + 'nsds5replicatedattributelist', 'nsds5replicatedattributelisttotal', + 'nsds5replicatimeout', 'nsds5replicaenabled' + ] + search_display_attributes = [ + 'cn', 'ipaReplTopoSegmentdirection', 'ipaReplTopoSegmentrightNode', + 'ipaReplTopoSegmentLeftNode' + ] + + label = _('Topology Segments') + label_singular = _('Topology Segment') + + takes_params = ( + Str( + 'cn', + maxlength=255, + cli_name='name', + primary_key=True, + label=_('Segment name'), + default_from=lambda iparepltoposegmentleftnode, iparepltoposegmentrightnode: + '%s-%s' % (iparepltoposegmentleftnode, iparepltoposegmentrightnode), + normalizer=lambda value: value.lower(), + doc=_('Arbitrary string identifying the segment'), + ), + Str( + 'iparepltoposegmentleftnode', + pattern='^[a-zA-Z0-9.][a-zA-Z0-9.-]{0,252}[a-zA-Z0-9.$-]?$', + pattern_errmsg='may only include letters, numbers, -, . and $', + maxlength=255, + cli_name='leftnode', + label=_('Left node'), + normalizer=lambda value: value.lower(), + doc=_('Left replication node - an IPA server'), + ), + Str( + 'iparepltoposegmentrightnode', + pattern='^[a-zA-Z0-9.][a-zA-Z0-9.-]{0,252}[a-zA-Z0-9.$-]?$', + pattern_errmsg='may only include letters, numbers, -, . and $', + maxlength=255, + cli_name='rightnode', + label=_('Right node'), + normalizer=lambda value: value.lower(), + doc=_('Right replication node - an IPA server'), + ), + StrEnum( + 'iparepltoposegmentdirection', + cli_name='direction', + label=_('Connectivity'), + values=(u'both', u'left-right', u'right-left', u'none'), + default=u'both', + doc=_('Direction of replication between left and right replication ' + 'node'), + ), + Str( + 'nsds5replicastripattrs?', + cli_name='stripattrs', + label=_('Attributes to strip'), + normalizer=lambda value: value.lower(), + doc=_('A space separated list of attributes which are removed from ' + 'replication updates.') + ), + Str( + 'nsds5replicatedattributelist?', + cli_name='replattrs', + label='Attributes to replicate', + doc=_('Attributes that are not replicated to a consumer server ' + 'during a fractional update. E.g., `(objectclass=*) ' + '$ EXCLUDE accountlockout memberof'), + ), + Str( + 'nsds5replicatedattributelisttotal?', + cli_name='replattrstotal', + label=_('Attributes for total update'), + doc=_('Attributes that are not replicated to a consumer server ' + 'during a total update. E.g. (objectclass=*) $ EXCLUDE ' + 'accountlockout'), + ), + Int( + 'nsds5replicatimeout?', + cli_name='timeout', + label=_('Session timeout'), + minvalue=0, + doc=_('Number of seconds outbound LDAP operations waits for a ' + 'response from the remote replica before timing out and ' + 'failing'), + ), + StrEnum( + 'nsds5replicaenabled?', + cli_name='enabled', + label=_('Replication agreement enabled'), + doc=_('Whether a replication agreement is active, meaning whether ' + 'replication is occurring per that agreement'), + values=(u'on', u'off'), + ), + ) + + def validate_nodes(self, ldap, dn, entry_attrs): + leftnode = entry_attrs.get('iparepltoposegmentleftnode') + rightnode = entry_attrs.get('iparepltoposegmentrightnode') + + if not leftnode and not rightnode: + return # nothing to check + + # check if nodes are IPA servers + masters = self.api.Command.server_find('', sizelimit=0)['result'] + m_hostnames = [master['cn'][0].lower() for master in masters] + + if leftnode and leftnode not in m_hostnames: + raise errors.ValidationError( + name='leftnode', + error=_('left node is not a topology node: %(leftnode)s') % + dict(leftnode=leftnode) + ) + + if rightnode and rightnode not in m_hostnames: + raise errors.ValidationError( + name='rightnode', + error=_('right node is not a topology node: %(rightnode)s') % + dict(rightnode=rightnode) + ) + + # prevent creation of reflexive relation + key = 'leftnode' + if not leftnode or not rightnode: # get missing end + _entry_attrs = ldap.get_entry(dn, ['*']) + if not leftnode: + key = 'rightnode' + leftnode = _entry_attrs['iparepltoposegmentleftnode'][0] + else: + rightnode = _entry_attrs['iparepltoposegmentrightnode'][0] + + if leftnode == rightnode: + raise errors.ValidationError( + name=key, + error=_('left node and right node must not be the same') + ) + + +@register() +class topologysegment_find(LDAPSearch): + __doc__ = _('Search for topology segments.') + + msg_summary = ngettext( + '%(count)d segment matched', + '%(count)d segments matched', 0 + ) + + +@register() +class topologysegment_add(LDAPCreate): + __doc__ = _('Add a new segment.') + + msg_summary = _('Added segment "%(value)s"') + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): + assert isinstance(dn, DN) + validate_domain_level(self.api) + self.obj.validate_nodes(ldap, dn, entry_attrs) + return dn + + +@register() +class topologysegment_del(LDAPDelete): + __doc__ = _('Delete a segment.') + + msg_summary = _('Deleted segment "%(value)s"') + + def pre_callback(self, ldap, dn, *keys, **options): + assert isinstance(dn, DN) + validate_domain_level(self.api) + return dn + + +@register() +class topologysegment_mod(LDAPUpdate): + __doc__ = _('Modify a segment.') + + msg_summary = _('Modified segment "%(value)s"') + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): + assert isinstance(dn, DN) + validate_domain_level(self.api) + self.obj.validate_nodes(ldap, dn, entry_attrs) + return dn + + +@register() +class topologysegment_refresh(LDAPQuery): + __doc__ = _('Request a replication refresh of specified node.') + + has_output = output.standard_value + msg_summary = _('%(value)s') + + takes_options = ( + Flag( + 'left?', + doc=_('Initialize left node'), + default=False, + ), + Flag( + 'right?', + doc=_('Initialize right node'), + default=False, + ), + Flag( + 'stop?', + doc=_('Stop already started refresh of chosen node(s)'), + default=False, + ), + ) + + def execute(self, *keys, **options): + dn = self.obj.get_dn(*keys, **options) + validate_domain_level(self.api) + + entry = self.obj.backend.get_entry( + dn, [ + 'nsds5beginreplicarefresh;left', + 'nsds5beginreplicarefresh;right' + ]) + + left = options.get('left') + right = options.get('right') + stop = options.get('stop') + action = u'start' + msg = _('Replication refresh for segment: "%(pkey)s" requested.') + if stop: + action = u'stop' + msg = _('Stopping of replication refresh for segment: "' + '%(pkey)s" requested.') + + if not left and not right: + raise errors.OptionError( + _('at least one node has to be specified') + ) + + if left: + entry['nsds5beginreplicarefresh;left'] = [action] + if right: + entry['nsds5beginreplicarefresh;right'] = [action] + + self.obj.backend.update_entry(entry) + + msg = msg % {'pkey': keys[-1]} + return dict( + result=True, + value=msg, + ) + + +@register() +class topologysegment_show(LDAPRetrieve): + __doc__ = _('Display a segment.') + + +@register() +class topologysuffix(LDAPObject): + """ + Suffix managed by the topology plugin. + """ + container_dn = api.env.container_topology + object_name = _('suffix') + object_name_plural = _('suffices') + object_class = ['iparepltopoconf'] + default_attributes = ['cn', 'ipaReplTopoConfRoot'] + search_display_attributes = ['cn', 'ipaReplTopoConfRoot'] + label = _('Topology suffices') + label_singular = _('Topology suffix') + + takes_params = ( + Str( + 'cn', + cli_name='name', + primary_key=True, + label=_('Suffix name'), + ), + Str( + 'iparepltopoconfroot', + maxlength=255, + cli_name='suffix', + label=_('LDAP suffix to be managed'), + normalizer=lambda value: value.lower(), + ), + ) + + +@register() +class topologysuffix_find(LDAPSearch): + __doc__ = _('Search for topology suffices.') + + msg_summary = ngettext( + '%(count)d topology suffix matched', + '%(count)d topology suffices matched', 0 + ) + + +@register() +class topologysuffix_del(LDAPDelete): + __doc__ = _('Delete a topology suffix.') + + msg_summary = _('Deleted topology suffix "%(value)s"') + + def pre_callback(self, ldap, dn, *keys, **options): + assert isinstance(dn, DN) + validate_domain_level(self.api) + return dn + + +@register() +class topologysuffix_add(LDAPCreate): + __doc__ = _('Add a new topology suffix to be managed.') + + msg_summary = _('Added topology suffix "%(value)s"') + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): + assert isinstance(dn, DN) + validate_domain_level(self.api) + return dn + + +@register() +class topologysuffix_mod(LDAPUpdate): + __doc__ = _('Modify a topology suffix.') + + msg_summary = _('Modified topology suffix "%(value)s"') + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): + assert isinstance(dn, DN) + validate_domain_level(self.api) + return dn + + +@register() +class topologysuffix_show(LDAPRetrieve): + __doc__ = _('Show managed suffix.')