diff options
author | Martin Kosek <mkosek@redhat.com> | 2012-01-12 09:37:36 +0100 |
---|---|---|
committer | Martin Kosek <mkosek@redhat.com> | 2012-01-12 10:46:55 +0100 |
commit | 55395770456e72fedd935b01a64c2f635bd81394 (patch) | |
tree | 06bdb5919c738fdee0b55da633ad9f4a8faa237b /ipalib | |
parent | 688f630c6be44ae3fb977ec0f280ffde9179fba8 (diff) | |
download | freeipa.git-55395770456e72fedd935b01a64c2f635bd81394.tar.gz freeipa.git-55395770456e72fedd935b01a64c2f635bd81394.tar.xz freeipa.git-55395770456e72fedd935b01a64c2f635bd81394.zip |
Create per-type DNS API
Use new structured DNSRecord parameters to generate per-type API
for all supported DNS RR types. This should help significantly
the end-user with manipulating complex DNS record type (MX, LOC,
etc.).
All enhancements are integrated to current DNS record commands:
1) dnsrecord-add
- Records can be either entered as a raw value (e.g. --mx-rec=
"1 srv1.example.com" for MX record) or per-part:
--mx-preference=1 --mx-exchanger=srv1.example.com
- CLI interactive help behavior was changed. It will ask for
a record type and then ask for all DNS record part values
(e.g. MX Preference value, MX Exchanger value).
2) dnsrecord-mod
- This command can now operate in 2 modes. When only a raw DNS
record is entered (e.g. --mx-rec="1 srv1.example.com") it
operates in standard mode and replaces any previous mxrecord
value with the --mx-rec value.
When any structured parameter (e.g. --mx-preference) is passed
it modifies just the specified parts of one mxrecord value
referred by --mx-rec:
--mx-rec="1 srv1.example.com" --mx-preference=2
- New interactive help has been implemented. It will ask for a
record to be modified (in the same manner as dnsrecord-del)
and then let user change DNS record part(s) for chosen
records.
3) All dnsrecord-* commands have now --structured option
- When this option is passed, instead of displaying raw DNS values
all DNS records are parsed and displayed per-part. Example:
$ ipa dnsrecord-show example.com @ --structured
Record name: @
Records:
Record type: MX
Record data: 0 server1.example.com.
MX Preference: 0
MX Exchanger: server1.example.com.
Record type: NS
Record data: ns1.example.com.
NS Hostname: ns1.example.com.
All API changes are compatible with clients without this patch.
https://fedorahosted.org/freeipa/ticket/2082
Diffstat (limited to 'ipalib')
-rw-r--r-- | ipalib/plugins/dns.py | 430 |
1 files changed, 383 insertions, 47 deletions
diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py index 33856c27..dfec70ec 100644 --- a/ipalib/plugins/dns.py +++ b/ipalib/plugins/dns.py @@ -25,7 +25,7 @@ import re from ipalib.request import context from ipalib import api, errors, output from ipalib import Command -from ipalib.parameters import Flag, Bool, Int, Float, Str, StrEnum +from ipalib.parameters import Flag, Bool, Int, Float, Str, StrEnum, Any from ipalib.plugins.baseldap import * from ipalib import _, ngettext from ipalib.util import validate_zonemgr, normalize_zonemgr, validate_hostname @@ -41,34 +41,50 @@ Manage DNS zone and resource records. EXAMPLES: Add new zone: - ipa dnszone-add example.com --name-server nameserver.example.com - --admin-email admin@example.com + ipa dnszone-add example.com --name-server=nameserver.example.com \\ + --admin-email=admin@example.com Modify the zone to allow dynamic updates for hosts own records in realm EXAMPLE.COM: ipa dnszone-mod example.com --dynamic-update=TRUE \\ --update-policy="grant EXAMPLE.COM krb5-self * A; grant EXAMPLE.COM krb5-self * AAAA;" Add new reverse zone specified by network IP address: - ipa dnszone-add --name-from-ip 80.142.15.0/24 - --name-server nameserver.example.com + ipa dnszone-add --name-from-ip=80.142.15.0/24 \\ + --name-server=nameserver.example.com Add second nameserver for example.com: - ipa dnsrecord-add example.com @ --ns-rec nameserver2.example.com + ipa dnsrecord-add example.com @ --ns-rec=nameserver2.example.com Add a mail server for example.com: - ipa dnsrecord-add example.com @ --mx-rec="10 mail2" + ipa dnsrecord-add example.com @ --mx-rec="10 mail1" + + Add another record using MX record specific options: + ipa dnsrecord-add example.com @ --mx-preference=20 --mx-exchanger=mail2 + + Add another record using interactive mode (started when dnsrecord-add, dnsrecord-mod, + or dnsrecord-del are executed with no options): + ipa dnsrecord-add example.com @ + Please choose a type of DNS resource record to be added + The most common types for this type of zone are: NS, MX, LOC + + DNS resource record type: MX + MX Preference: 30 + MX Exchanger: mail3 + Record name: example.com + MX record: 10 mail1, 20 mail2, 30 mail3 + NS record: nameserver.example.com., nameserver2.example.com. Delete previously added nameserver from example.com: - ipa dnsrecord-del example.com @ --ns-rec nameserver2.example.com + ipa dnsrecord-del example.com @ --ns-rec=nameserver2.example.com. Add LOC record for example.com: - ipa dnsrecord-add example.com @ --loc-rec "49 11 42.4 N 16 36 29.6 E 227.64m" + ipa dnsrecord-add example.com @ --loc-rec="49 11 42.4 N 16 36 29.6 E 227.64m" Add new A record for www.example.com: (random IP) - ipa dnsrecord-add example.com www --a-rec 80.142.15.2 + ipa dnsrecord-add example.com www --a-rec=80.142.15.2 Add new PTR record for www.example.com - ipa dnsrecord-add 15.142.80.in-addr.arpa. 2 --ptr-rec www.example.com. + ipa dnsrecord-add 15.142.80.in-addr.arpa. 2 --ptr-rec=www.example.com. Add new SRV records for LDAP servers. Three quarters of the requests should go to fast.example.com, one quarter to slow.example.com. If neither @@ -77,16 +93,27 @@ EXAMPLES: ipa dnsrecord-add example.com _ldap._tcp --srv-rec="0 1 389 slow.example.com" ipa dnsrecord-add example.com _ldap._tcp --srv-rec="1 1 389 backup.example.com" - When dnsrecord-add command is executed with no option to add a specific record - an interactive mode is started. The mode interactively prompts for the most - typical record types for the respective zone: - ipa dnsrecord-add example.com www - [A record]: 1.2.3.4,11.22.33.44 (2 interactively entered random IPs) - [AAAA record]: (no AAAA address entered) - Record name: www - A record: 1.2.3.4, 11.22.33.44 + The interactive mode can be used for easy modification: + ipa dnsrecord-mod example.com _ldap._tcp + No option to modify specific record provided. + Current DNS record contents: - The interactive mode can also be used for deleting the DNS records: + SRV record: 0 3 389 fast.example.com, 0 1 389 slow.example.com, 1 1 389 backup.example.com + + Modify SRV record '0 3 389 fast.example.com'? Yes/No (default No): + Modify SRV record '0 1 389 slow.example.com'? Yes/No (default No): y + SRV Priority [0]: (keep the default value) + SRV Weight [1]: 2 (modified value) + SRV Port [389]: (keep the default value) + SRV Target [slow.example.com]: (keep the default value) + 1 SRV record skipped. Only one value per DNS record type can be modified at one time. + Record name: _ldap._tcp + SRV record: 0 3 389 fast.example.com, 1 1 389 backup.example.com, 0 2 389 slow.example.com + + After this modification, three fifths of the requests should go to + fast.example.com and two fifths to slow.example.com. + + An example of the interactive mode for dnsrecord-del command: ipa dnsrecord-del example.com www No option to delete specific record provided. Delete all? Yes/No (default No): (do not delete all records) @@ -109,7 +136,7 @@ EXAMPLES: ipa dnsrecord-find example.com www Find A records with value 10.10.0.1 in zone example.com - ipa dnsrecord-find example.com --a-rec 10.10.0.1 + ipa dnsrecord-find example.com --a-rec=10.10.0.1 Show records for resource www in zone example.com ipa dnsrecord-show example.com www @@ -251,6 +278,7 @@ class DNSRecord(Str): kw.setdefault('cli_name', '%s_rec' % self.rrtype.lower()) kw.setdefault('label', self.label_format % self.rrtype) kw.setdefault('doc', self.doc_format % self.rrtype) + kw.setdefault('option_group', self.option_group_format % self.rrtype) kw['csv'] = True if not self.supported: @@ -264,12 +292,32 @@ class DNSRecord(Str): return None return tuple(values) + def get_parts_from_kw(self, kw, raise_on_none=True): + part_names = tuple(self.part_name_format % (self.rrtype.lower(), part.name) \ + for part in self.parts) + vals = tuple(kw.get(part_name) for part_name in part_names) + + if all(val is None for val in vals): + return + + if raise_on_none: + for val_id,val in enumerate(vals): + if val is None and self.parts[val_id].required: + cli_name = self.cli_name_format % (self.rrtype.lower(), self.parts[val_id].name) + raise errors.ConversionError(name=self.name, + error=_("'%s' is a required part of DNS record") % cli_name) + + return vals + + def _validate_parts(self, parts): + if len(parts) != len(self.parts): + raise errors.ValidationError(name=self.name, + error=_("Invalid number of parts!")) + def _convert_scalar(self, value, index=None): if isinstance(value, (tuple, list)): # convert parsed values to the string - if len(value) != len(self.parts): - raise errors.ConversionError(name=self.name, index=index, - error=_("Invalid number of parts!")) + self._validate_parts(value) return u" ".join(super(DNSRecord, self)._convert_scalar(v, index) \ for v in value if v is not None) return super(DNSRecord, self)._convert_scalar(value, index) @@ -313,6 +361,9 @@ class DNSRecord(Str): if value is None: return + if value is None: + return + if not self.supported: return _('DNS RR type "%s" is not supported by bind-dyndb-ldap plugin') \ % self.rrtype @@ -342,6 +393,45 @@ class DNSRecord(Str): part.validate(val) return None + def get_parts(self): + if self.parts is None: + return tuple() + return tuple(part.clone_rename(self.part_name_format % (self.rrtype.lower(), part.name), + cli_name=self.cli_name_format % (self.rrtype.lower(), part.name), + label=self.part_label_format % (self.rrtype, unicode(part.label)), + required=False, + option_group=self.option_group_format % self.rrtype, + flags=(tuple(part.flags) + ('dnsrecord_part', 'virtual_attribute',))) \ + for part in self.parts) + + def prompt_parts(self, backend, mod_dnsvalue=None): + mod_parts = None + if mod_dnsvalue is not None: + mod_parts = self._get_part_values(mod_dnsvalue) + + user_options = {} + if self.parts is None: + return user_options + + for part_id, part in enumerate(self.parts): + name = self.part_name_format % (self.rrtype.lower(), part.name) + label = self.part_label_format % (self.rrtype, unicode(part.label)) + optional = not part.required + if mod_parts: + default = mod_parts[part_id] + else: + default = None + + raw = backend.textui.prompt(label, + optional=optional, + default=default) + if not raw.strip(): + raw = default + + user_options[name] = part(raw) + + return user_options + class ARecord(DNSRecord): rrtype = 'A' rfc = 1035 @@ -614,6 +704,32 @@ class LOCRecord(DNSRecord): return tuple(x.strip() if x is not None else x for x in m.groups()) + def _validate_parts(self, parts): + super(LOCRecord, self)._validate_parts(parts) + + # create part_name -> part_id map first + part_name_map = dict((part.name, part_id) \ + for part_id,part in enumerate(self.parts)) + + requirements = ( ('lat_sec', 'lat_min'), + ('lon_sec', 'lon_min'), + ('h_precision', 'size'), + ('v_precision', 'h_precision', 'size') ) + + for req in requirements: + target_part = req[0] + + if parts[part_name_map[target_part]] is not None: + required_parts = req[1:] + if any(parts[part_name_map[part]] is None for part in required_parts): + target_cli_name = self.cli_name_format % (self.rrtype.lower(), req[0]) + required_cli_names = [ self.cli_name_format % (self.rrtype.lower(), part) + for part in req[1:] ] + error = _("'%(required)s' must not be empty when '%(name)s' is set") % \ + dict(required=', '.join(required_cli_names), + name=target_cli_name) + raise errors.ValidationError(name=self.name, error=error) + class MXRecord(DNSRecord): rrtype = 'MX' rfc = 1035 @@ -863,7 +979,7 @@ class TXTRecord(DNSRecord): ), ) -_dns_record_options = ( +_dns_records = ( ARecord(), AAAARecord(), A6Record(), @@ -900,6 +1016,28 @@ _dns_record_options = ( TXTRecord(), ) +def __dns_record_options_iter(): + for opt in (Any('dnsrecords?', + label=_('Records'), + flags=['no_create', 'no_search', 'no_update'],), + Str('dnstype?', + label=_('Record type'), + flags=['no_create', 'no_search', 'no_update'],), + Str('dnsdata?', + label=_('Record data'), + flags=['no_create', 'no_search', 'no_update'],)): + # These 3 options are used in --structured format. They are defined + # rather in takes_params than has_output_params because of their + # order - they should be printed to CLI before any DNS part param + yield opt + for option in _dns_records: + yield option + + for part in option.get_parts(): + yield part + +_dns_record_options = tuple(__dns_record_options_iter()) + # dictionary of valid reverse zone -> number of address components _valid_reverse_zones = { '.in-addr.arpa.' : 4, @@ -1257,6 +1395,11 @@ class dnsrecord(LDAPObject): ), ) + _dns_record_options + structured_flag = Flag('structured', + label=_('Structured'), + doc=_('Parse all raw DNS records and return them in a structured way'), + ) + def _nsrecord_pre_callback(self, ldap, dn, entry_attrs, *keys, **options): if options.get('force', False): return dn @@ -1351,19 +1494,29 @@ class dnsrecord(LDAPObject): return dict((attr, val) for attr,val in entry_attrs.iteritems() \ if attr in self.params and not self.params[attr].primary_key) - def prompt_record_options(self, rec_type_list): - user_options = {} - # ask for all usual record types - for rec_type in rec_type_list: - rec_option = self.get_record_option(rec_type) - if rec_option is None: - continue - raw = self.Backend.textui.prompt(rec_option.label,optional=True) - rec_value = rec_option(raw) - if rec_value is not None: - user_options[rec_option.name] = rec_value + def postprocess_record(self, record, **options): + if options.get('structured', False): + for attr in record.keys(): + try: + param = self.params[attr] + except KeyError: + continue - return user_options + if not isinstance(param, DNSRecord): + continue + parts_params = param.get_parts() + + for dnsvalue in record[attr]: + dnsentry = { + u'dnstype' : unicode(param.rrtype), + u'dnsdata' : dnsvalue + } + values = param._get_part_values(dnsvalue) + for val_id, val in enumerate(values): + if val is not None: + dnsentry[parts_params[val_id].name] = val + record.setdefault('dnsrecords', []).append(dnsentry) + del record[attr] api.register(dnsrecord) @@ -1379,6 +1532,7 @@ class dnsrecord_add(LDAPCreate): flags=['no_option', 'no_output'], doc=_('force NS record creation even if its hostname is not in DNS'), ), + dnsrecord.structured_flag, ) def args_options_2_entry(self, *keys, **options): @@ -1396,14 +1550,36 @@ class dnsrecord_add(LDAPCreate): # check zone type if kw['idnsname'] == _dns_zone_record: - top_record_types = _zone_top_record_types + common_types = u', '.join(_zone_top_record_types) elif zone_is_reverse(kw['dnszoneidnsname']): - top_record_types = _rev_top_record_types + common_types = u', '.join(_rev_top_record_types) else: - top_record_types = _top_record_types + common_types = u', '.join(_top_record_types) + + self.Backend.textui.print_plain(_(u'Please choose a type of DNS resource record to be added')) + self.Backend.textui.print_plain(_(u'The most common types for this type of zone are: %s\n') %\ + common_types) + + ok = False + while not ok: + rrtype = self.Backend.textui.prompt(_(u'DNS resource record type')) + + if rrtype is None: + return - # ask for all usual record types - user_options = self.obj.prompt_record_options(top_record_types) + try: + name = '%srecord' % rrtype.lower() + param = self.params[name] + + if not isinstance(param, DNSRecord): + raise ValueError() + except KeyError, ValueError: + all_types = u', '.join(_record_types) + self.Backend.textui.print_plain(_(u'Invalid type. Allowed values are: %s') % all_types) + continue + ok = True + + user_options = param.prompt_parts(self.Backend) kw.update(user_options) def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): @@ -1412,6 +1588,20 @@ class dnsrecord_add(LDAPCreate): if hasattr(self.obj, rtype_cb): dn = getattr(self.obj, rtype_cb)(ldap, dn, entry_attrs, *keys, **options) + # check if any record part was added + for option in options: + option_part_re = re.match(r'([a-z]+)_part_', option) + + if option_part_re is not None: + record_option = self.obj.get_record_option(option_part_re.group(1)) + if record_option.name in entry_attrs: + # this record was already entered + continue + + parts = record_option.get_parts_from_kw(options) + dnsvalue = [record_option._convert_scalar(parts)] + entry_attrs[record_option.name] = dnsvalue + try: (dn_, old_entry) = ldap.get_entry( dn, entry_attrs.keys(), @@ -1441,6 +1631,11 @@ class dnsrecord_add(LDAPCreate): return raise exc + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + self.obj.postprocess_record(entry_attrs, **options) + + return dn + api.register(dnsrecord_add) @@ -1449,6 +1644,10 @@ class dnsrecord_mod(LDAPUpdate): no_option_msg = 'No options to modify a specific record provided.' + takes_options = LDAPUpdate.takes_options + ( + dnsrecord.structured_flag, + ) + def args_options_2_entry(self, *keys, **options): self.obj.has_cli_options(options, self.no_option_msg, True) return super(dnsrecord_mod, self).args_options_2_entry(*keys, **options) @@ -1461,6 +1660,54 @@ class dnsrecord_mod(LDAPUpdate): if hasattr(self.obj, rtype_cb): dn = getattr(self.obj, rtype_cb)(ldap, dn, entry_attrs, *keys, **options) + # check if any attr should be updated using structured instead of replaced + # format is recordname : (old_value, new_parts) + updated_attrs = {} + for attr in entry_attrs: + param = self.params[attr] + if not isinstance(param, DNSRecord): + continue + + parts = param.get_parts_from_kw(options, raise_on_none=False) + + if parts is None: + # old-style modification + continue + + if isinstance(entry_attrs[attr], (tuple, list)): + if len(entry_attrs[attr]) > 1: + raise errors.ValidationError(name=param.name, + error=_('DNS records can be only updated one at a time')) + old_value = entry_attrs[attr][0] + else: + old_value = entry_attrs[attr] + + updated_attrs[attr] = (old_value, parts) + + if len(updated_attrs): + try: + (dn_, old_entry) = ldap.get_entry( + dn, updated_attrs.keys(), + normalize=self.obj.normalize_dn) + except errors.NotFound: + self.obj.handle_not_found(*keys) + + for attr in updated_attrs: + param = self.params[attr] + old_dnsvalue, new_parts = updated_attrs[attr] + + if old_dnsvalue not in old_entry.get(attr, []): + raise errors.AttrValueNotFound(attr=attr, + value=old_dnsvalue) + old_entry[attr].remove(old_dnsvalue) + + old_parts = param._get_part_values(old_dnsvalue) + modified_parts = tuple(part if part is not None else old_parts[part_id] \ + for part_id,part in enumerate(new_parts)) + + new_dnsvalue = [param._convert_scalar(modified_parts)] + entry_attrs[attr] = list(set(old_entry[attr] + new_dnsvalue)) + return dn def execute(self, *keys, **options): @@ -1484,6 +1731,61 @@ class dnsrecord_mod(LDAPUpdate): return self.obj.methods.delentry(*keys) return result + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + self.obj.postprocess_record(entry_attrs, **options) + + def interactive_prompt_callback(self, kw): + try: + self.obj.has_cli_options(kw, self.no_option_msg, True) + except errors.OptionError: + pass + else: + # some record type entered, skip this helper + return + + # get DNS record first so that the NotFound exception is raised + # before the helper would start + dns_record = api.Command['dnsrecord_show'](kw['dnszoneidnsname'], kw['idnsname'])['result'] + rec_types = [rec_type for rec_type in dns_record if rec_type in _record_attributes] + + self.Backend.textui.print_plain(_("No option to modify specific record provided.")) + + # ask user for records to be removed + self.Backend.textui.print_plain(_(u'Current DNS record contents:\n')) + record_params = [] + + for attr in dns_record: + try: + param = self.params[attr] + except KeyError: + continue + if not isinstance(param, DNSRecord): + continue + + record_params.append(param) + rec_type_content = u', '.join(dns_record[param.name]) + self.Backend.textui.print_plain(u'%s: %s' % (param.label, rec_type_content)) + self.Backend.textui.print_plain(u'') + + # ask what records to remove + for param in record_params: + rec_values = list(dns_record[param.name]) + for rec_value in dns_record[param.name]: + rec_values.remove(rec_value) + mod_value = self.Backend.textui.prompt_yesno( + _("Modify %(name)s '%(value)s'?") % dict(name=param.label, value=rec_value), default=False) + if mod_value is True: + user_options = param.prompt_parts(self.Backend, mod_dnsvalue=rec_value) + kw[param.name] = [rec_value] + kw.update(user_options) + + if rec_values: + self.Backend.textui.print_plain(ngettext( + u'%(count)d %(type)s record skipped. Only one value per DNS record type can be modified at one time.', + u'%(count)d %(type)s records skipped. Only one value per DNS record type can be modified at one time.', + 0) % dict(count=len(rec_values), type=param.rrtype)) + break + api.register(dnsrecord_mod) @@ -1502,13 +1804,24 @@ class dnsrecord_del(LDAPUpdate): no_option_msg = _('Neither --del-all nor options to delete a specific record provided.\n'\ "Command help may be consulted for all supported record types.") + takes_options = ( Flag('del_all', default=False, label=_('Delete all associated records'), ), + dnsrecord.structured_flag, ) + def get_options(self): + for option in super(dnsrecord_del, self).get_options(): + if 'dnsrecord_part' in option.flags: + continue + elif isinstance(option, DNSRecord): + yield option.clone(option_group=None) + continue + yield option + def pre_callback(self, ldap, dn, entry_attrs, *keys, **options): try: (dn_, old_entry) = ldap.get_entry( @@ -1559,6 +1872,11 @@ class dnsrecord_del(LDAPUpdate): return self.obj.methods.delentry(*keys) return result + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + if self.obj.is_pkey_zone_record(*keys): + entry_attrs[self.obj.primary_key.name] = [_dns_zone_record] + self.obj.postprocess_record(entry_attrs, **options) + def args_options_2_entry(self, *keys, **options): self.obj.has_cli_options(options, self.no_option_msg) return super(dnsrecord_del, self).args_options_2_entry(*keys, **options) @@ -1589,11 +1907,18 @@ class dnsrecord_del(LDAPUpdate): # ask user for records to be removed self.Backend.textui.print_plain(_(u'Current DNS record contents:\n')) present_params = [] - for param in self.params: - if param.name in _record_attributes and param.name in dns_record: - present_params.append(param) - rec_type_content = u', '.join(dns_record[param.name]) - self.Backend.textui.print_plain(u'%s: %s' % (param.label, rec_type_content)) + + for attr in dns_record: + try: + param = self.params[attr] + except KeyError: + continue + if not isinstance(param, DNSRecord): + continue + + present_params.append(param) + rec_type_content = u', '.join(dns_record[param.name]) + self.Backend.textui.print_plain(u'%s: %s' % (param.label, rec_type_content)) self.Backend.textui.print_plain(u'') # ask what records to remove @@ -1615,9 +1940,14 @@ api.register(dnsrecord_del) class dnsrecord_show(LDAPRetrieve): __doc__ = _('Display DNS resource.') + takes_options = LDAPRetrieve.takes_options + ( + dnsrecord.structured_flag, + ) + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): if self.obj.is_pkey_zone_record(*keys): entry_attrs[self.obj.primary_key.name] = [_dns_zone_record] + self.obj.postprocess_record(entry_attrs, **options) return dn api.register(dnsrecord_show) @@ -1626,6 +1956,10 @@ api.register(dnsrecord_show) class dnsrecord_find(LDAPSearch): __doc__ = _('Search for DNS resources.') + takes_options = LDAPSearch.takes_options + ( + dnsrecord.structured_flag, + ) + def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *args, **options): # include zone record (root entry) in the search return (filter, base_dn, ldap.SCOPE_SUBTREE) @@ -1636,6 +1970,8 @@ class dnsrecord_find(LDAPSearch): zone_dn = zone_obj.get_dn(args[0]) if entries[0][0] == zone_dn: entries[0][1][zone_obj.primary_key.name] = [_dns_zone_record] + for entry in entries: + self.obj.postprocess_record(entry[1], **options) api.register(dnsrecord_find) |