On 09.06.2016 12:21, Martin Basti wrote:
Hello,

here is WIP version of generator for IPA DNS records and locations, that is responsible for creating and updating all IPA records for all masters.

Please note that this is not finished yet and some methods may not work.


Patch attached



Updated, this version actually works
From 511adfcc1c33ae8d49afb4fc21a6eb1cf2b211b2 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Wed, 8 Jun 2016 17:53:58 +0200
Subject: [PATCH 1/2] DNS Location: generator for records

WIP
---
 install/share/60ipadns.ldif |   2 +
 ipalib/dns.py               | 334 ++++++++++++++++++++++++++++++++++++++++++++
 ipaserver/plugins/dns.py    |   1 +
 3 files changed, 337 insertions(+)

diff --git a/install/share/60ipadns.ldif b/install/share/60ipadns.ldif
index 5bfed905566bdbfe4e011e218c328701ce854943..0eb90d68353fd94605e70070bb885e4158281235 100644
--- a/install/share/60ipadns.ldif
+++ b/install/share/60ipadns.ldif
@@ -70,6 +70,7 @@ attributeTypes: ( 2.16.840.1.113730.3.8.5.25 NAME 'idnsSecKeyRevoke' DESC 'DNSKE
 attributeTypes: ( 2.16.840.1.113730.3.8.5.26 NAME 'idnsSecKeySep' DESC 'DNSKEY SEP flag (equivalent to bit 15): RFC 4035' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.1' )
 attributeTypes: ( 2.16.840.1.113730.3.8.5.27 NAME 'idnsSecAlgorithm' DESC 'DNSKEY algorithm: string used as mnemonic' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE X-ORIGIN 'IPA v4.1' )
 attributeTypes: ( 2.16.840.1.113730.3.8.5.28 NAME 'idnsSecKeyRef' DESC 'PKCS#11 URI of the key' EQUALITY caseExactMatch SINGLE-VALUE SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.1' )
+attributeTypes: ( 2.16.840.1.113730.3.8.5.29 NAME 'idnsTemplateAttribute' DESC 'Template attribute for dynamic attribute generation' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 X-ORIGIN 'IPA v4.4' )
 attributeTypes: ( 2.16.840.1.113730.3.8.11.74 NAME 'ipaDNSVersion' DESC 'IPA DNS data version' EQUALITY integerMatch ORDERING integerOrderingMatch SINGLE-VALUE SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 X-ORIGIN 'IPA v4.3' )
 attributeTypes: ( 2.16.840.1.113730.3.8.5.32 NAME 'ipaLocation' DESC 'Reference to IPA location' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'IPA v4.4' )
 attributeTypes: ( 2.16.840.1.113730.3.8.5.33 NAME 'ipaLocationWeight' DESC 'Weight for the server in IPA location' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.4' )
@@ -79,6 +80,7 @@ objectClasses: ( 2.16.840.1.113730.3.8.6.2 NAME 'idnsConfigObject' DESC 'DNS glo
 objectClasses: ( 2.16.840.1.113730.3.8.12.18 NAME 'ipaDNSZone' SUP top AUXILIARY MUST idnsName MAY managedBy X-ORIGIN 'IPA v3' )
 objectClasses: ( 2.16.840.1.113730.3.8.6.3 NAME 'idnsForwardZone' DESC 'Forward Zone class' SUP top STRUCTURAL MUST ( idnsName $ idnsZoneActive ) MAY ( idnsForwarders $ idnsForwardPolicy ) )
 objectClasses: ( 2.16.840.1.113730.3.8.6.4 NAME 'idnsSecKey' DESC 'DNSSEC key metadata' STRUCTURAL MUST ( idnsSecKeyRef $ idnsSecKeyCreated $ idnsSecAlgorithm ) MAY ( idnsSecKeyPublish $ idnsSecKeyActivate $ idnsSecKeyInactive $ idnsSecKeyDelete $ idnsSecKeyZone $ idnsSecKeyRevoke $ idnsSecKeySep $ cn ) X-ORIGIN 'IPA v4.1' )
+objectClasses: ( 2.16.840.1.113730.3.8.6.5 NAME 'idnsTemplateObject' DESC 'Template object for dynamic DNS attribute generation' AUXILIARY MUST ( idnsTemplateAttribute ) X-ORIGIN 'IPA v4.4' )
 objectClasses: ( 2.16.840.1.113730.3.8.12.36 NAME 'ipaDNSContainer' DESC 'IPA DNS container' AUXILIARY MUST ( ipaDNSVersion ) X-ORIGIN 'IPA v4.3' )
 objectClasses: ( 2.16.840.1.113730.3.8.6.7 NAME 'ipaLocationObject' DESC 'Object for storing IPA server location' STRUCTURAL MUST ( idnsName ) MAY ( description ) X-ORIGIN 'IPA v4.4' )
 objectClasses: ( 2.16.840.1.113730.3.8.6.8 NAME 'ipaLocationMember' DESC 'Member object of IPA location' AUXILIARY MAY ( ipaLocation $ ipaLocationWeight ) X-ORIGIN 'IPA v4.4' )
diff --git a/ipalib/dns.py b/ipalib/dns.py
index 54a4c24a08c974d8e905e4630d91d2381c39778d..a234c9b57d9e7b4cbd07f74f5051b8ab0ac2124f 100644
--- a/ipalib/dns.py
+++ b/ipalib/dns.py
@@ -18,9 +18,19 @@
 # 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 collections import defaultdict
+from dns import (
+    rrset,
+    rdataclass,
+    rdatatype,
+)
+from dns.rdtypes.IN.SRV import SRV
 
 from ipalib import errors
+from ipapython.dnsutil import DNSName, resolve_rrsets
 
 # dnsrecord param name formats
 record_name_format = '%srecord'
@@ -98,3 +108,327 @@ def iterate_rrparams_by_parts(cmd, kw, skip_extra=False):
         if rrparam.name not in processed:
             processed.append(rrparam.name)
             yield rrparam
+
+
+class DNSRecordsGenerator(object):
+
+    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),
+    )
+
+    # 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):
+        self.__init_data()
+
+    def __get_server_attrs(self, hostname, allowed_roles=None):
+        """
+        Returns weight, location, and intersection of its roles and allowed
+        roles
+        :param hostname: server hostname
+        :param allowed_roles: *set* of server roles that are allowed, if None
+        all roles will be used
+        :return: (weight, location, intersection of roles and allowed roles)
+        """
+        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 __get_srv_records(
+        self, hostname, rname_port_map,
+        weight=100, priority=0, location=None
+    ):
+        assert isinstance(hostname, DNSName)
+        assert isinstance(priority, int)
+        assert isinstance(weight, int)
+
+        rrsets_list = []
+
+        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)
+
+            # FIXME: use TTL from config
+            rrsets_list.append(rrset.from_rdata(r_name, 86400, rd))
+        return rrsets_list
+
+    def __get_ca_records_from_hostname(self, hostname):
+        assert isinstance(hostname, DNSName) and hostname.is_absolute()
+        rrsets = resolve_rrsets(hostname, ['A', 'AAAA'])
+        for rrset in rrsets:
+            rrset.name = (
+                DNSName('ipa-ca') + self.domain_abs
+            )
+            # FIXME: use TTL from config
+            rrset.ttl = 86400
+        return rrsets
+
+    def _sort_rrsets(self, rrsets, result_dict):
+        for rr in rrsets:
+            records = result_dict.setdefault(rr.name, [])
+            records.append(rr)
+
+    def get_base_dns_records(self, servers=None, roles=None,
+                             include_master_role=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
+        :return: dictionary containing record name as key and list of RRSets
+        as value
+        """
+        records = {}
+        if servers is None:
+            servers = self.servers_data.keys()
+
+        for server in servers:
+            self._sort_rrsets(
+                self._get_base_dns_records_for_server(
+                    server, roles=roles,
+                    include_master_role=include_master_role
+                ),
+                records
+            )
+        return records
+
+    def get_location_dns_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
+        :return: dictionary containing record name as key and list of RRSets
+        as value
+        """
+        records = {}
+        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._sort_rrsets(
+                self._get_location_dns_records_for_server(
+                    server, locations, roles=roles,
+                    include_master_role=include_master_role
+                ),
+                records
+            )
+        return records
+
+    def _get_base_dns_records_for_server(self, hostname, roles=None,
+                                         include_master_role=True):
+        base_records_list = []
+        server = self.servers_data[hostname]
+        if roles:
+            eff_roles = server['roles'] & roles
+        else:
+            eff_roles = server['roles']
+        hostname_abs = DNSName(hostname).make_absolute()
+
+        # get master records
+        if include_master_role:
+            base_records_list.extend(
+                self.__get_srv_records(
+                    hostname_abs,
+                    self.IPA_DEFAULT_MASTER_SRV_REC,
+                    weight=server['weight']
+                )
+            )
+
+        if 'CA server' in eff_roles:
+            base_records_list.extend(
+                self.__get_ca_records_from_hostname(hostname_abs)
+            )
+
+        if 'AD trust controller' in eff_roles:
+            base_records_list.extend(
+                self.__get_srv_records(
+                    hostname_abs,
+                    self.IPA_DEFAULT_ADTRUST_SRV_REC,
+                    weight=server['weight']
+                )
+            )
+
+        return base_records_list
+
+    def _get_location_dns_records_for_server(
+            self, hostname, locations, roles=None, include_master_role=True):
+        """
+        Function returns list of DNS records for server with proper locations
+        mappings
+        :param api_instance: instance of API
+        :param hostname: server hostname
+        :return: dict with keys as record name, and list of rrsets as values
+        """
+        location_records_list = []
+        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:
+                location_records_list.extend(
+                    self.__get_srv_records(
+                        hostname_abs,
+                        self.IPA_DEFAULT_MASTER_SRV_REC,
+                        weight=server['weight'],
+                        priority=priority,
+                        location=location
+                    )
+                )
+
+            if 'AD trust controller' in eff_roles:
+                location_records_list.extend(
+                    self.__get_srv_records(
+                        hostname_abs,
+                        self.IPA_DEFAULT_ADTRUST_SRV_REC,
+                        weight=server['weight'],
+                        priority=priority,
+                        location=location
+                    )
+                )
+
+        return location_records_list
+
+
+    def __prepare_records_update_dict(self, rrsets):
+        update_dict = defaultdict(list)
+        for rrset in rrsets:
+            for rdata in rrset.items:
+                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, rrsets, set_cname_template=True
+    ):
+        update_dict = self.__prepare_records_update_dict(rrsets)
+        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_add(self.domain_abs,
+                        record_name, **cname_template)
+                except errors.EmptyModlist:
+                    pass
+        except errors.EmptyModlist:
+            pass
+
+    def update_dns_records(self):
+        names_requiring_cname_templates = set(
+            rec[0].derelativize(self.domain_abs) for rec in (
+                self.IPA_DEFAULT_MASTER_SRV_REC +
+                self.IPA_DEFAULT_ADTRUST_SRV_REC
+            )
+        )
+
+        base_records = self.get_base_dns_records()
+        for record_name, rrsets in base_records.items():
+            set_cname_template = record_name in names_requiring_cname_templates
+            self.__update_dns_records(record_name, rrsets, set_cname_template)
+
+        location_records = self.get_location_dns_records()
+        for record_name, rrsets in location_records.items():
+            self.__update_dns_records(
+                record_name, rrsets, set_cname_template=False)
+
+        return base_records, location_records
diff --git a/ipaserver/plugins/dns.py b/ipaserver/plugins/dns.py
index db179079319a98ed27801ca55ae7a699c458e767..52242d66cefc03645ddbf66a1a217eb51da91e95 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
-- 
2.5.5

From 0aa2044a5c05fb5b2575cc942d722ee25b05e1d6 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Thu, 9 Jun 2016 20:28:29 +0200
Subject: [PATCH 2/2] DNS Location: return error when ipa domain is not managed
 by IPA

---
 ipalib/dns.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/ipalib/dns.py b/ipalib/dns.py
index a234c9b57d9e7b4cbd07f74f5051b8ab0ac2124f..5003e635103bf98dbe8e0d249abc09fd60d36af0 100644
--- a/ipalib/dns.py
+++ b/ipalib/dns.py
@@ -110,6 +110,10 @@ def iterate_rrparams_by_parts(cmd, kw, skip_extra=False):
             yield rrparam
 
 
+class IPADomainIsNotManagedByIPAError(Exception):
+    pass
+
+
 class DNSRecordsGenerator(object):
 
     IPA_DEFAULT_MASTER_SRV_REC = (
@@ -414,6 +418,12 @@ class DNSRecordsGenerator(object):
             pass
 
     def update_dns_records(self):
+        try:
+            self.api_instance.Command.dnszone_show(self.domain_abs,
+                                                   pkey_only=True)
+        except errors.NotFound:
+            raise IPADomainIsNotManagedByIPAError()
+
         names_requiring_cname_templates = set(
             rec[0].derelativize(self.domain_abs) for rec in (
                 self.IPA_DEFAULT_MASTER_SRV_REC +
-- 
2.5.5

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to