From 87c23ba029df9227384b3f5e2028f3f0e429e9ab Mon Sep 17 00:00:00 2001 From: Martin Basti Date: Jun 17 2016 13:22:24 +0000 Subject: 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 Reviewed-By: Jan Cholasta --- diff --git a/ipalib/dns.py b/ipalib/dns.py index 54a4c24..55e45a0 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 . +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 0000000..7e5ad18 --- /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 5df363c..dea2ce9 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