summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPavel Zuna <pzuna@redhat.com>2009-04-27 21:05:20 +0200
committerRob Crittenden <rcritten@redhat.com>2009-04-30 16:17:49 -0400
commit36c239cda44c3e816a3ffd95957f2d49f434f62b (patch)
tree2d579d482fc8879a1b8e705dbb9e045d73f0f147
parent8eabf068fbb5108c8b16391215000b073cfd79e8 (diff)
downloadfreeipa-36c239cda44c3e816a3ffd95957f2d49f434f62b.tar.gz
freeipa-36c239cda44c3e816a3ffd95957f2d49f434f62b.tar.xz
freeipa-36c239cda44c3e816a3ffd95957f2d49f434f62b.zip
Add DNS management plugin port to the new ldap backend.
-rw-r--r--ipalib/plugins/dns2.py797
1 files changed, 797 insertions, 0 deletions
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 <pzuna@redhat.com>
+#
+# 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)
+