From 36c239cda44c3e816a3ffd95957f2d49f434f62b Mon Sep 17 00:00:00 2001 From: Pavel Zuna Date: Mon, 27 Apr 2009 21:05:20 +0200 Subject: Add DNS management plugin port to the new ldap backend. --- ipalib/plugins/dns2.py | 797 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 797 insertions(+) create mode 100644 ipalib/plugins/dns2.py diff --git a/ipalib/plugins/dns2.py b/ipalib/plugins/dns2.py new file mode 100644 index 000000000..b478a5bcb --- /dev/null +++ b/ipalib/plugins/dns2.py @@ -0,0 +1,797 @@ +# Authors: +# Pavel Zuna +# +# Copyright (C) 2009 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" +Domain Name System (DNS) Plugin + +Implements a set of commands useful for manipulating DNS records used by +the BIND LDAP plugin. + +EXAMPLES: + + Add new zone; + ipa dns2-create example.com nameserver.example.com admin@example.com + + Add second nameserver for example.com: + ipa dns2-add-rr example.com @ NS nameserver2.example.com + + Delete previously added nameserver from example.com: + ipa dns2-del-rr example.com @ NS nameserver2.example.com + + Add new A record for www.example.com: (random IP) + ipa dns2-add-rr example.com www A 80.142.15.2 + + Show zone example.com: + ipa dns2-show example.com + + Find zone with 'example' in it's domain name: + ipa dns2-find example + + Find records for resources with 'www' in their name in zone example.com: + ipa dns2-find-rr example.com www + + Find A records for resource www in zone example.com + ipa dns2-find-rr example.com --resource www --type A + + Show records for resource www in zone example.com + ipa dns2-show-rr example.com www + + Delete zone example.com with all resource records: + ipa dns2-delete example.com +""" + +# A few notes about the LDAP schema to make this plugin more understandable: +# - idnsRecord object is a HOSTNAME with one or more resource records +# - idnsZone object is a idnsRecord object with mandatory SOA record +# it basically makes the assumption that ZONE == DOMAINNAME + SOA record +# resource records can be stored in both idnsZone and idnsRecord objects + +import time + +from ipalib import api, crud, errors +from ipalib import Object, Command +from ipalib import Flag, Int, Str, StrEnum + +# parent DN +_zone_container_dn = api.env.container_dns + +# supported resource record types +_record_types = ( + u'A', u'AAAA', u'A6', u'AFSDB', u'CERT', u'CNAME', u'DNAME', + u'DS', u'HINFO', u'KEY', u'KX', u'LOC', u'MD', u'MINFO', u'MX', + u'NAPTR', u'NS', u'NSEC', u'NXT', u'PTR', u'RRSIG', u'SSHFP', + u'SRV', u'TXT', +) + +# supported DNS classes, IN = internet, rest is almost never used +_record_classes = (u'IN', u'CS', u'CH', u'HS') + +# attributes displayed by default for resource records +_record_default_attributes = ['%srecord' % r for r in _record_types] +_record_default_attributes.append('idnsname') + +# attributes displayed by default for zones +_zone_default_attributes = [ + 'idnsname', 'idnszoneactive', 'idnssoamname', 'idnssoarname', + 'idnssoaserial', 'idnssoarefresh', 'idnssoaretry', 'idnssoaexpire', + 'idnssoaminimum' +] + + +# build zone dn +def _get_zone_dn(ldap, idnsname): + rdn = ldap.make_rdn_from_attr('idnsName', idnsname) + return ldap.make_dn_from_rdn(rdn, _zone_container_dn) + +# build dn for entry with record +def _get_record_dn(ldap, zone, idnsname): + parent_dn = _get_zone_dn(ldap, zone) + if idnsname == '@' or idnsname == zone: + return parent_dn + rdn = ldap.make_rdn_from_attr('idnsName', idnsname) + return ldap.make_dn_from_rdn(rdn, parent_dn) + + +class dns2(Object): + """DNS zone/SOA record object.""" + + takes_params = ( + Str('idnsname', + cli_name='name', + doc='zone name (FQDN)', + normalizer=lambda value: value.lower(), + primary_key=True, + ), + Str('idnssoamname', + cli_name='name_server', + doc='authoritative name server', + ), + Str('idnssoarname', + cli_name='admin_email', + doc='administrator e-mail address', + default_from=lambda idnsname: 'root.%s' % idnsname, + normalizer=lambda value: value.replace('@', '.'), + ), + Int('idnssoaserial?', + cli_name='serial', + doc='SOA serial', + ), + Int('idnssoarefresh?', + cli_name='refresh', + doc='SOA refresh', + ), + Int('idnssoaretry?', + cli_name='retry', + doc='SOA retry', + ), + Int('idnssoaexpire?', + cli_name='expire', + doc='SOA expire', + ), + Int('idnssoaminimum?', + cli_name='minimum', + doc='SOA minimum', + ), + Int('dnsttl?', + cli_name='ttl', + doc='SOA time to live', + ), + StrEnum('dnsclass?', + cli_name='class', + doc='SOA class', + values=_record_classes, + ), + Flag('idnsallowdynupdate', + cli_name='allow_dynupdate', + doc='allow dynamic update?', + ), + ) + +api.register(dns2) + + +class dns2_create(crud.Create): + """ + Create new DNS zone/SOA record. + """ + + def execute(self, *args, **options): + assert self.api.env.use_ldap2, 'use_ldap2 is False' + ldap = self.Backend.ldap2 + idnsname = args[0] + + # build entry attributes + entry_attrs = self.args_options_2_entry(*args, **options) + + # build entry DN + dn = _get_zone_dn(ldap, idnsname) + + # fill in required attributes + entry_attrs['objectclass'] = ['top', 'idnsRecord', 'idnsZone'] + entry_attrs['idnszoneactive'] = True + + # fill default values, build SOA serial from current date + soa_serial = int('%s01' % time.strftime('%Y%d%m')) + entry_attrs.setdefault('idnssoaserial', soa_serial) + entry_attrs.setdefault('idnssoarefresh', 3600) + entry_attrs.setdefault('idnssoaretry', 900) + entry_attrs.setdefault('idnssoaexpire', 1209600) + entry_attrs.setdefault('idnssoaminimum', 3600) + + # create zone entry + ldap.add_entry(dn, entry_attrs) + + # get zone entry with created attributes for output + return ldap.get_entry(dn, entry_attrs.keys()) + + def output_for_cli(self, textui, result, *args, **options): + (dn, entry_attrs) = result + idnsname = args[0] + + textui.print_name(self.name) + textui.print_attribute('dn', dn) + textui.print_entry(entry_attrs) + textui.print_dashed('Created DNS zone "%s".' % idnsname) + +api.register(dns2_create) + + +class dns2_delete(crud.Delete): + """ + Delete existing DNS zone/SOA record. + """ + + def execute(self, *args, **options): + assert self.api.env.use_ldap2, 'use_ldap2 is False' + ldap = self.api.Backend.ldap2 + idnsname = args[0] + + # build zone entry DN + dn = _get_zone_dn(ldap, idnsname) + # just check if zone exists for now + ldap.get_entry(dn, ['']) + + # retrieve all subentries of zone - records + try: + entries = ldap.find_entries(None, [''], dn, ldap.SCOPE_ONELEVEL) + except errors.NotFound: + entries = tuple() + + # kill'em all, records first + for e in entries: + ldap.delete_entry(e[0]) + ldap.delete_entry(dn) + + # return something positive + return True + + def output_for_cli(self, textui, result, *args, **options): + textui.print_name(self.name) + textui.print_dashed('Deleted DNS zone "%s".' % args[0]) + +api.register(dns2_delete) + + +class dns2_mod(crud.Update): + """ + Modify DNS zone/SOA record. + """ + + def execute(self, *args, **options): + assert self.api.env.use_ldap2, 'use_ldap2 is False' + ldap = self.api.Backend.ldap2 + idnsname = args[0] + + # build entry attributes, don't include idnsname! + entry_attrs = self.args_options_2_entry(*tuple(), **options) + + # build entry DN + dn = _get_zone_dn(ldap, idnsname) + + # update zone entry + ldap.update_entry(dn, entry_attrs) + + # get zone entry with modified + default attributes for output + return ldap.get_entry( + dn, (entry_attrs.keys() + _zone_default_attributes) + ) + + def output_for_cli(self, textui, result, *args, **options): + (dn, entry_attrs) = result + idnsname = args[0] + + textui.print_name(self.name) + textui.print_attribute('dn', dn) + textui.print_entry(entry_attrs) + textui.print_dashed('Modified DNS zone "%s".' % idnsname) + +api.register(dns2_mod) + + +class dns2_find(crud.Search): + """ + Search for DNS zones/SOA records. + """ + + takes_options = ( + Flag('all', + doc='retrieve all attributes', + ), + ) + + def execute(self, term, **options): + assert self.api.env.use_ldap2, 'use_ldap2 is False' + ldap = self.api.Backend.ldap2 + + # build search filter + filter = ldap.make_filter_from_attr('idnsName', term, exact=False) + + # select attributes we want to retrieve + if options['all']: + attrs_list = ['*'] + else: + attrs_list = _zone_default_attributes + + # get matching entries + try: + entries = ldap.find_entries( + filter, attrs_list, _zone_container_dn, ldap.SCOPE_ONELEVEL + ) + except errors.NotFound: + entries = tuple() + + return entries + + def output_for_cli(self, textui, result, term, **options): + textui.print_name(self.name) + for e in result: + (dn, entry_attrs) = e + textui.print_attribute('dn', dn) + textui.print_entry(entry_attrs) + textui.print_plain('') + textui.print_count( + len(result), '%i DNS zone matched.', '%i DNS zones matched.' + ) + +api.register(dns2_find) + + +class dns2_show(crud.Retrieve): + """ + Display DNS zone/SOA record. + """ + + takes_options = ( + Flag('all', + doc='retrieve all attributes', + ), + ) + + def execute(self, idnsname, **options): + assert self.api.env.use_ldap2, 'use_ldap2 is False' + ldap = self.api.Backend.ldap2 + + # build entry DN + dn = _get_zone_dn(ldap, idnsname) + + # select attributes we want to retrieve + if options['all']: + attrs_list = ['*'] + else: + attrs_list = _zone_default_attributes + + return ldap.get_entry(dn, attrs_list) + + def output_for_cli(self, textui, result, *args, **options): + (dn, entry_attrs) = result + + textui.print_name(self.name) + textui.print_attribute('dn', dn) + textui.print_entry(entry_attrs) + +api.register(dns2_show) + + +class dns2_enable(Command): + """ + Activate DNS zone. + """ + + takes_args = ( + Str('zone', + cli_name='zone', + doc='zone name', + normalizer=lambda value: value.lower(), + ), + ) + + def execute(self, zone): + assert self.api.env.use_ldap2, 'use_ldap2 is False' + ldap = self.api.Backend.ldap2 + + # build entry DN + dn = _get_zone_dn(ldap, zone) + + # activate! + try: + ldap.update_entry(dn, {'idnszoneactive': True}) + except errors.EmptyModlist: + pass + + # return something positive + return True + + def output_for_cli(self, textui, result, zone): + textui.print_name(self.name) + textui.print_dashed('Activated DNS zone "%s".' % zone) + +api.register(dns2_enable) + + +class dns2_disable(Command): + """ + Deactivate DNS zone. + """ + + takes_args = ( + Str('zone', + cli_name='zone', + doc='zone name', + normalizer=lambda value: value.lower(), + ), + ) + + def execute(self, zone): + assert self.api.env.use_ldap2, 'use_ldap2 is False' + ldap = self.api.Backend.ldap2 + + # build entry DN + dn = _get_zone_dn(ldap, zone) + + # deactivate! + try: + ldap.update_entry(dn, {'idnszoneactive': False}) + except errors.EmptyModlist: + pass + + # return something positive + return True + + def output_for_cli(self, textui, result, zone): + textui.print_name(self.name) + textui.print_dashed('Deactivated DNS zone "%s".' % zone) + +api.register(dns2_disable) + + +class dns2_add_rr(Command): + """ + Add new DNS resource record. + """ + + takes_args = ( + Str('zone', + cli_name='zone', + doc='zone name', + normalizer=lambda value: value.lower(), + ), + Str('idnsname', + cli_name='resource', + doc='resource name', + default_from=lambda zone: zone.lower(), + attribute=True, + ), + StrEnum('type', + cli_name='type', + doc='record type', + values=_record_types, + ), + Str('data', + cli_name='data', + doc='type-specific data', + ), + ) + + takes_options = ( + Int('dnsttl?', + cli_name='ttl', + doc='time to live', + attribute=True, + ), + StrEnum('dnsclass?', + cli_name='class', + doc='class', + values=_record_classes, + attribute=True, + ), + ) + + def execute(self, zone, idnsname, type, data, **options): + assert self.api.env.use_ldap2, 'use_ldap2 is False' + ldap = self.api.Backend.ldap2 + attr = '%srecord' % type + + # build entry DN + dn = _get_record_dn(ldap, zone, idnsname) + + # get resource entry where to store the new record + try: + (dn, entry_attrs) = ldap.get_entry(dn, [attr]) + except errors.NotFound: + if idnsname != '@' and idnsname != zone: + # resource entry doesn't exist, check if zone exists + zone_dn = _get_zone_dn(ldap, zone) + ldap.get_entry(zone_dn, ['']) + # it does, create new resource entry + + # build entry attributes + entry_attrs = self.args_options_2_entry( + (idnsname, ), **options + ) + + # fill in required attributes + entry_attrs['objectclass'] = ['top', 'idnsRecord'] + + # fill in the record + entry_attrs[attr] = data + + # create the entry + ldap.add_entry(dn, entry_attrs) + + # get entry with created attributes for output + return ldap.get_entry(dn, entry_attrs.keys()) + + # zone doesn't exist + raise + # resource entry already exists, create a modlist for the new record + + # convert entry_attrs keys to lowercase + #entry_attrs = dict( + # (k.lower(), v) for (k, v) in entry_attrs.iteritems() + #) + + # get new value for record attribute + attr_value = entry_attrs.get(attr, []) + attr_value.append(data) + + ldap.update_entry(dn, {attr: attr_value}) + # get entry with updated attribute for output + return ldap.get_entry(dn, ['idnsname', attr]) + + def output_for_cli(self, textui, result, zone, idnsname, type, data, + **options): + (dn, entry_attrs) = result + output = '"%s %s %s" to zone "%s"' % ( + idnsname, type, data, zone, + ) + + textui.print_name(self.name) + textui.print_attribute('dn', dn) + textui.print_entry(entry_attrs) + textui.print_dashed('Added DNS resource record %s.' % output) + +api.register(dns2_add_rr) + + +class dns2_del_rr(Command): + """ + Delete DNS resource record. + """ + + takes_args = ( + Str('zone', + cli_name='zone', + doc='zone name', + normalizer=lambda value: value.lower(), + ), + Str('idnsname', + cli_name='resource', + doc='resource name', + default_from=lambda zone: zone.lower(), + attribute=True, + ), + StrEnum('type', + cli_name='type', + doc='record type', + values=_record_types, + ), + Str('data', + cli_name='data', + doc='type-specific data', + ), + ) + + def execute(self, zone, idnsname, type, data): + assert self.api.env.use_ldap2, 'use_ldap2 is False' + ldap = self.api.Backend.ldap2 + attr = '%srecord' % type + + # build entry DN + dn = _get_record_dn(ldap, zone, idnsname) + + # get resource entry with the record we're trying to delete + (dn, entry_attrs) = ldap.get_entry(dn) + + # convert entry_attrs keys to lowercase + entry_attrs = dict( + (k.lower(), v) for (k, v) in entry_attrs.iteritems() + ) + + # get new value for record attribute + attr_value = entry_attrs.get(attr.lower(), []) + try: + attr_value.remove(data) + except ValueError: + raise errors.NotFound(message=u'resource record not found') + + # check if it's worth to keep this entry in LDAP + if 'idnsZone' not in entry_attrs['objectclass']: + # get a list of all meaningful record attributes + record_attrs = [] + for (k, v) in entry_attrs.iteritems(): + if k.endswith('record') and v: + record_attrs.append(k) + # check if the list is empty + if not record_attrs: + # it's not + ldap.delete_entry(dn) + return True + + ldap.update_entry(dn, {attr: attr_value}) + # get entry with updated attribute for output + return ldap.get_entry(dn, ['idnsname', attr]) + + def output_for_cli(self, textui, result, zone, idnsname, type, data): + output = '"%s %s %s" from zone "%s"' % ( + idnsname, type, data, zone, + ) + + textui.print_name(self.name) + if not isinstance(result, bool): + (dn, entry_attrs) = result + textui.print_attribute('dn', dn) + textui.print_entry(entry_attrs) + textui.print_dashed('Deleted DNS resource record %s' % output) + +api.register(dns2_del_rr) + + +class dns2_find_rr(Command): + """ + Search for DNS resource records. + """ + + takes_args = ( + Str('zone', + cli_name='zone', + doc='zone name', + normalizer=lambda value: value.lower(), + ), + Str('criteria?', + cli_name='criteria', + doc='search criteria', + ), + ) + + takes_options = ( + Str('idnsname?', + cli_name='resource', + doc='resource name', + default_from=lambda zone: zone.lower(), + ), + StrEnum('type?', + cli_name='type', + doc='record type', + values=_record_types, + ), + Str('data?', + cli_name='data', + doc='type-specific data', + ), + Flag('all', + doc='retrieve all attributes', + ), + ) + + def execute(self, zone, term, **options): + assert self.api.env.use_ldap2, 'use_ldap2 is False' + ldap = self.api.Backend.ldap2 + if 'type' in options: + attr = '%srecord' % options['type'] + else: + attr = None + + # build base dn for search + base_dn = _get_zone_dn(ldap, zone) + + # build search keywords + search_kw = {} + if 'data' in options: + if attr is not None: + # user is looking for a certain record type + search_kw[attr] = options['data'] + else: + # search in all record types + for a in _record_default_attributes: + search_kw[a] = term + if 'idnsname' in options: + idnsname = options['idnsname'] + if idnsname == '@': + search_kw['idnsname'] = zone + else: + search_kw['idnsname'] = idnsname + + # build search filter + filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL) + if term: + search_kw = {} + for a in _record_default_attributes: + search_kw[a] = term + term_filter = ldap.make_filter(search_kw, exact=False) + filter = ldap.combine_filters((filter, term_filter), ldap.MATCH_ALL) + self.log.info(filter) + + # select attributes we want to retrieve + if options['all']: + attrs_list = ['*'] + elif attr is not None: + attrs_list = [attr] + else: + attrs_list = _record_default_attributes + + # get matching entries + try: + entries = ldap.find_entries(filter, attrs_list, base_dn) + except errors.NotFound: + entries = tuple() + + # if the user is looking for a certain record type, don't display + # entries that do not contain it + if attr is not None: + related_entries = [] + for e in entries: + entry_attrs = e[1] + if attr in entry_attrs: + related_entries.append(e) + entries = related_entries + + return entries + + def output_for_cli(self, textui, result, zone, term, **options): + textui.print_name(self.name) + for e in result: + (dn, entry_attrs) = e + textui.print_attribute('dn', dn) + textui.print_entry(entry_attrs) + textui.print_plain('') + textui.print_count( + len(result), '%i DNS resource record matched.', + '%i DNS resource records matched.' + ) + +api.register(dns2_find_rr) + + +class dns2_show_rr(Command): + """ + Show existing DNS resource records. + """ + + takes_args = ( + Str('zone', + cli_name='zone', + doc='zone name', + normalizer=lambda value: value.lower(), + ), + Str('idnsname', + cli_name='resource', + doc='resource name', + normalizer=lambda value: value.lower(), + ), + ) + + takes_options = ( + Flag('all', + doc='retrieve all attributes', + ), + ) + + def execute(self, zone, idnsname, **options): + # shows all records associated with resource + ldap = self.api.Backend.ldap2 + + # build entry DN + dn = _get_record_dn(ldap, zone, idnsname) + + # select attributes we want to retrieve + if options['all']: + attrs_list = ['*'] + else: + attrs_list = _record_default_attributes + + return ldap.get_entry(dn, attrs_list) + + def output_for_cli(self, textui, result, zone, idnsname, **options): + (dn, entry_attrs) = result + + textui.print_name(self.name) + textui.print_attribute('dn', dn) + textui.print_entry(entry_attrs) + +api.register(dns2_show_rr) + -- cgit