summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Basti <mbasti@redhat.com>2016-06-08 17:53:58 +0200
committerMartin Basti <mbasti@redhat.com>2016-06-17 15:22:24 +0200
commit87c23ba029df9227384b3f5e2028f3f0e429e9ab (patch)
tree76e0c9c0aae52b685f44f7b120c7adc0d7fbc39c
parent745a2e6471b27faabeb5479b9d2845b18606d8b0 (diff)
downloadfreeipa-87c23ba029df9227384b3f5e2028f3f0e429e9ab.tar.gz
freeipa-87c23ba029df9227384b3f5e2028f3f0e429e9ab.tar.xz
freeipa-87c23ba029df9227384b3f5e2028f3f0e429e9ab.zip
DNS Locations: DNS data management
Adding module that allows to work with IPA DNS system records: * getting system records * updating system records * work with DNS locations https://fedorahosted.org/freeipa/ticket/2008 Reviewed-By: Petr Spacek <pspacek@redhat.com> Reviewed-By: Jan Cholasta <jcholast@redhat.com>
-rw-r--r--ipalib/dns.py2
-rw-r--r--ipaserver/dns_data_management.py380
-rw-r--r--ipaserver/plugins/dns.py1
3 files changed, 383 insertions, 0 deletions
diff --git a/ipalib/dns.py b/ipalib/dns.py
index 54a4c24a0..55e45a094 100644
--- a/ipalib/dns.py
+++ b/ipalib/dns.py
@@ -18,6 +18,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+from __future__ import absolute_import
+
import re
from ipalib import errors
diff --git a/ipaserver/dns_data_management.py b/ipaserver/dns_data_management.py
new file mode 100644
index 000000000..7e5ad18e8
--- /dev/null
+++ b/ipaserver/dns_data_management.py
@@ -0,0 +1,380 @@
+#
+# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
+#
+
+from __future__ import absolute_import
+
+from collections import defaultdict
+from dns import (
+ rdataclass,
+ rdatatype,
+ zone,
+)
+from dns.rdtypes.IN.SRV import SRV
+from dns.rdtypes.ANY.TXT import TXT
+
+from ipalib import errors
+from ipalib.dns import record_name_format
+from ipapython.dnsutil import DNSName, resolve_rrsets
+
+IPA_DEFAULT_MASTER_SRV_REC = (
+ # srv record name, port
+ (DNSName(u'_ldap._tcp'), 389),
+ (DNSName(u'_kerberos._tcp'), 88),
+ (DNSName(u'_kerberos._udp'), 88),
+ (DNSName(u'_kerberos-master._tcp'), 88),
+ (DNSName(u'_kerberos-master._udp'), 88),
+ (DNSName(u'_kpasswd._tcp'), 464),
+ (DNSName(u'_kpasswd._udp'), 464),
+)
+
+IPA_DEFAULT_ADTRUST_SRV_REC = (
+ # srv record name, port
+ (DNSName(u'_ldap._tcp.Default-First-Site-Name._sites.dc._msdcs'), 389),
+ (DNSName(u'_ldap._tcp.dc._msdcs'), 389),
+ (DNSName(u'_kerberos._tcp.Default-First-Site-Name._sites.dc._msdcs'), 88),
+ (DNSName(u'_kerberos._udp.Default-First-Site-Name._sites.dc._msdcs'), 88),
+ (DNSName(u'_kerberos._tcp.dc._msdcs'), 88),
+ (DNSName(u'_kerberos._udp.dc._msdcs'), 88),
+)
+
+
+class IPADomainIsNotManagedByIPAError(Exception):
+ pass
+
+
+class IPASystemRecords(object):
+
+ # fixme do it configurable
+ PRIORITY_HIGH = 0
+ PRIORITY_LOW = 50
+
+ def __init__(self, api_instance):
+ self.api_instance = api_instance
+ self.domain_abs = DNSName(self.api_instance.env.domain).make_absolute()
+ self.servers_data = {}
+ self.__init_data()
+
+ def reload_data(self):
+ """
+ After any change made to IPA servers, this method must be called to
+ update data in the object, otherwise invalid records may be
+ created/updated
+ """
+ self.__init_data()
+
+ def __get_server_attrs(self, hostname):
+ server_result = self.api_instance.Command.server_show(hostname)['result']
+ weight = int(server_result.get('ipalocationweight', [u'100'])[0])
+ location = server_result.get('ipalocation_location', [None])[0]
+ roles = set(server_result.get('enabled_role_servrole', ()))
+
+ return weight, location, roles
+
+ def __init_data(self):
+ self.servers_data = {}
+
+ servers_result = self.api_instance.Command.server_find(
+ pkey_only=True)['result']
+ servers = [s['cn'][0] for s in servers_result]
+ for s in servers:
+ weight, location, roles = self.__get_server_attrs(s)
+ self.servers_data[s] = {
+ 'weight': weight,
+ 'location': location,
+ 'roles': roles,
+ }
+
+ def __add_srv_records(
+ self, zone_obj, hostname, rname_port_map,
+ weight=100, priority=0, location=None
+ ):
+ assert isinstance(hostname, DNSName)
+ assert isinstance(priority, int)
+ assert isinstance(weight, int)
+
+ if location:
+ suffix = (
+ location + DNSName('_locations') + self.domain_abs
+ )
+ else:
+ suffix = self.domain_abs
+
+ for name, port in rname_port_map:
+ rd = SRV(
+ rdataclass.IN, rdatatype.SRV,
+ priority,
+ weight,
+ port,
+ hostname.make_absolute()
+ )
+
+ r_name = name.derelativize(suffix)
+
+ rdataset = zone_obj.get_rdataset(
+ r_name, rdatatype.SRV, create=True)
+ rdataset.add(rd, ttl=86400) # FIXME: use TTL from config
+
+ def __add_ca_records_from_hostname(self, zone_obj, hostname):
+ assert isinstance(hostname, DNSName) and hostname.is_absolute()
+ r_name = DNSName('ipa-ca') + self.domain_abs
+ rrsets = resolve_rrsets(hostname, (rdatatype.A, rdatatype.AAAA))
+ for rrset in rrsets:
+ for rd in rrset:
+ rdataset = zone_obj.get_rdataset(
+ r_name, rd.rdtype, create=True)
+ rdataset.add(rd, ttl=86400) # FIXME: use TTL from config
+
+ def __add_kerberos_txt_rec(self, zone_obj):
+ # FIXME: with external DNS, this should generate records for all
+ # realmdomains
+ r_name = DNSName('_kerberos') + self.domain_abs
+ rd = TXT(rdataclass.IN, rdatatype.TXT, [self.api_instance.env.realm])
+ rdataset = zone_obj.get_rdataset(
+ r_name, rdatatype.TXT, create=True
+ )
+ rdataset.add(rd, ttl=86400) # FIXME: use TTL from config
+
+ def _add_base_dns_records_for_server(
+ self, zone_obj, hostname, roles=None, include_master_role=True,
+ include_kerberos_realm=True,
+ ):
+ server = self.servers_data[hostname]
+ if roles:
+ eff_roles = server['roles'] & set(roles)
+ else:
+ eff_roles = server['roles']
+ hostname_abs = DNSName(hostname).make_absolute()
+
+ if include_kerberos_realm:
+ self.__add_kerberos_txt_rec(zone_obj)
+
+ # get master records
+ if include_master_role:
+ self.__add_srv_records(
+ zone_obj,
+ hostname_abs,
+ IPA_DEFAULT_MASTER_SRV_REC,
+ weight=server['weight']
+ )
+
+ if 'CA server' in eff_roles:
+ self.__add_ca_records_from_hostname(zone_obj, hostname_abs)
+
+ if 'AD trust controller' in eff_roles:
+ self.__add_srv_records(
+ zone_obj,
+ hostname_abs,
+ IPA_DEFAULT_ADTRUST_SRV_REC,
+ weight=server['weight']
+ )
+
+ def _get_location_dns_records_for_server(
+ self, zone_obj, hostname, locations,
+ roles=None, include_master_role=True):
+ server = self.servers_data[hostname]
+ if roles:
+ eff_roles = server['roles'] & roles
+ else:
+ eff_roles = server['roles']
+ hostname_abs = DNSName(hostname).make_absolute()
+
+ # generate locations specific records
+ for location in locations:
+ if location == self.servers_data[hostname]['location']:
+ priority = self.PRIORITY_HIGH
+ else:
+ priority = self.PRIORITY_LOW
+
+ if include_master_role:
+ self.__add_srv_records(
+ zone_obj,
+ hostname_abs,
+ IPA_DEFAULT_MASTER_SRV_REC,
+ weight=server['weight'],
+ priority=priority,
+ location=location
+ )
+
+ if 'AD trust controller' in eff_roles:
+ self.__add_srv_records(
+ zone_obj,
+ hostname_abs,
+ IPA_DEFAULT_ADTRUST_SRV_REC,
+ weight=server['weight'],
+ priority=priority,
+ location=location
+ )
+
+ return zone_obj
+
+ def __prepare_records_update_dict(self, node):
+ update_dict = defaultdict(list)
+ for rdataset in node:
+ for rdata in rdataset:
+ option_name = (record_name_format % rdatatype.to_text(
+ rdata.rdtype).lower())
+ update_dict[option_name].append(rdata.to_text())
+ return update_dict
+
+ def __update_dns_records(
+ self, record_name, nodes, set_cname_template=True
+ ):
+ update_dict = self.__prepare_records_update_dict(nodes)
+ cname_template = {
+ 'addattr': [u'objectclass=idnsTemplateObject'],
+ 'setattr': [
+ u'idnsTemplateAttribute;cnamerecord=%s'
+ u'.\{substitutionvariable_ipalocation\}._locations' %
+ record_name.relativize(self.domain_abs)
+ ]
+ }
+ try:
+ if set_cname_template:
+ # only srv records should have configured cname templates
+ update_dict.update(cname_template)
+ self.api_instance.Command.dnsrecord_mod(
+ self.domain_abs, record_name,
+ **update_dict
+ )
+ except errors.NotFound:
+ # because internal API magic, addattr and setattr doesn't work with
+ # dnsrecord-add well, use dnsrecord-mod instead later
+ update_dict.pop('addattr', None)
+ update_dict.pop('setattr', None)
+
+ self.api_instance.Command.dnsrecord_add(
+ self.domain_abs, record_name, **update_dict)
+
+ if set_cname_template:
+ try:
+ self.api_instance.Command.dnsrecord_mod(
+ self.domain_abs,
+ record_name, **cname_template)
+ except errors.EmptyModlist:
+ pass
+ except errors.EmptyModlist:
+ pass
+
+ def get_base_records(
+ self, servers=None, roles=None, include_master_role=True,
+ include_kerberos_realm=True
+ ):
+ """
+ Generate IPA service records for specific servers and roles
+ :param servers: list of server which will be used in records,
+ if None all IPA servers will be used
+ :param roles: roles for which DNS records will be generated,
+ if None all roles will be used
+ :param include_master_role: generate records required by IPA master
+ role
+ :return: dns.zone.Zone object that contains base DNS records
+ """
+
+ zone_obj = zone.Zone(self.domain_abs, relativize=False)
+ if servers is None:
+ servers = self.servers_data.keys()
+
+ for server in servers:
+ self._add_base_dns_records_for_server(zone_obj, server,
+ roles=roles, include_master_role=include_master_role,
+ include_kerberos_realm=include_kerberos_realm
+ )
+ return zone_obj
+
+ def get_locations_records(
+ self, servers=None, roles=None, include_master_role=True):
+ """
+ Generate IPA location records for specific servers and roles.
+ :param servers: list of server which will be used in records,
+ if None all IPA servers will be used
+ :param roles: roles for which DNS records will be generated,
+ if None all roles will be used
+ :param include_master_role: generate records required by IPA master
+ role
+ :return: dns.zone.Zone object that contains location DNS records
+ """
+ zone_obj = zone.Zone(self.domain_abs, relativize=False)
+ if servers is None:
+ servers_result = self.api_instance.Command.server_find(
+ pkey_only=True)['result']
+ servers = [s['cn'][0] for s in servers_result]
+
+ locations_result = self.api_instance.Command.location_find()['result']
+ locations = [l['idnsname'][0] for l in locations_result]
+
+ for server in servers:
+ self._get_location_dns_records_for_server(
+ zone_obj, server,
+ locations, roles=roles,
+ include_master_role=include_master_role)
+ return zone_obj
+
+ def update_base_records(self):
+ """
+ Update base DNS records for IPA services
+ :return: [(record_name, node), ...], [(record_name, node, error), ...]
+ where the first list contains successfully updated records, and the
+ second list contains failed updates with particular exceptions
+ """
+ fail = []
+ success = []
+ names_requiring_cname_templates = set(
+ rec[0].derelativize(self.domain_abs) for rec in (
+ IPA_DEFAULT_MASTER_SRV_REC +
+ IPA_DEFAULT_ADTRUST_SRV_REC
+ )
+ )
+
+ base_zone = self.get_base_records()
+ for record_name, node in base_zone.items():
+ set_cname_template = record_name in names_requiring_cname_templates
+ try:
+ self.__update_dns_records(
+ record_name, node, set_cname_template)
+ except errors.PublicError as e:
+ fail.append((record_name, node, e))
+ else:
+ success.append((record_name, node))
+ return success, fail
+
+ def update_locations_records(self):
+ """
+ Update locations DNS records for IPA services
+ :return: [(record_name, node), ...], [(record_name, node, error), ...]
+ where the first list contains successfully updated records, and the
+ second list contains failed updates with particular exceptions
+ """
+ fail = []
+ success = []
+
+ location_zone = self.get_locations_records()
+ for record_name, nodes in location_zone.items():
+ try:
+ self.__update_dns_records(
+ record_name, nodes,
+ set_cname_template=False)
+ except errors.PublicError as e:
+ fail.append((record_name, nodes, e))
+ else:
+ success.append((record_name, nodes))
+ return success, fail
+
+ def update_dns_records(self):
+ """
+ Update all IPA DNS records
+ :return: (sucessfully_updated_base_records, failed_base_records,
+ sucessfully_updated_locations_records, failed_locations_records)
+ For format see update_base_records or update_locations_method
+ :raise IPADomainIsNotManagedByIPAError: if IPA domain is not managed by
+ IPA DNS
+ """
+ try:
+ self.api_instance.Command.dnszone_show(self.domain_abs)
+ except errors.NotFound:
+ raise IPADomainIsNotManagedByIPAError()
+
+ return (
+ self.update_base_records(),
+ self.update_locations_records()
+ )
diff --git a/ipaserver/plugins/dns.py b/ipaserver/plugins/dns.py
index 5df363cc2..dea2ce9b8 100644
--- a/ipaserver/plugins/dns.py
+++ b/ipaserver/plugins/dns.py
@@ -2957,6 +2957,7 @@ class dnsrecord(LDAPObject):
object_name = _('DNS resource record')
object_name_plural = _('DNS resource records')
object_class = ['top', 'idnsrecord']
+ possible_objectclasses = ['idnsTemplateObject']
permission_filter_objectclasses = ['idnsrecord']
default_attributes = ['idnsname'] + _record_attributes
rdn_is_primary_key = True