On 25.5.2016 12:50, Martin Basti wrote:
> 
> 
> On 20.05.2016 12:19, Petr Spacek wrote:
>> On 11.5.2016 12:08, Martin Basti wrote:
>>>
>>> On 03.05.2016 14:59, Petr Spacek wrote:
>>>> Hello,
>>>>
>>>> DNS upgrade: change forwarding policy to "only" if private IPs are used.
>>>>
>>>> https://fedorahosted.org/freeipa/ticket/5710
>>>>
>>>> This is the upgrade part. I will add one more patch to print a warning in
>>>> dnsforwardzone* commands to avoid surprises. Please do not close the ticket
>>>> yet.
>>>>
>>>>
>>>>
>>> 1)
>>> Upgrade failed with 'BindInstance' object has no attribute
>>> 'named_conf_get_directive'
>>> IPA server upgrade failed: Inspect /var/log/ipaupgrade.log and run command
>>> ipa-server-upgrade manually.
>>> ('IPA upgrade failed.', 1)
>>> The ipa-server-upgrade command failed. See /var/log/ipaupgrade.log for more
>>> information
>>>
>>> 2016-05-11T08:26:20Z ERROR Upgrade failed with 'BindInstance' object has no
>>> attribute 'named_conf_get_directive'
>>> 2016-05-11T08:26:20Z DEBUG Traceback (most recent call last):
>>>    File
>>> "/usr/lib/python2.7/site-packages/ipaserver/install/upgradeinstance.py", 
>>> line
>>> 213, in __upgrade
>>>      self.modified = (ld.update(self.files) or self.modified)
>>>    File "/usr/lib/python2.7/site-packages/ipaserver/install/ldapupdate.py",
>>> line 917, in update
>>>      self._run_updates(all_updates)
>>>    File "/usr/lib/python2.7/site-packages/ipaserver/install/ldapupdate.py",
>>> line 889, in _run_updates
>>>      self._run_update_plugin(update['plugin'])
>>>    File "/usr/lib/python2.7/site-packages/ipaserver/install/ldapupdate.py",
>>> line 862, in _run_update_plugin
>>>      restart_ds, updates = self.api.Updater[plugin_name]()
>>>    File "/usr/lib/python2.7/site-packages/ipalib/frontend.py", line 1418, in
>>> __call__
>>>      return self.execute(**options)
>>>    File "/usr/lib/python2.7/site-packages/ipaserver/install/plugins/dns.py",
>>> line 547, in execute
>>>      self.update_global_named_conf_forwarder(bind)
>>>    File "/usr/lib/python2.7/site-packages/ipaserver/install/plugins/dns.py",
>>> line 508, in update_global_named_conf_forwarder
>>>      if bind.named_conf_get_directive(
>>> AttributeError: 'BindInstance' object has no attribute
>>> 'named_conf_get_directive'
>>>
>>> 2016-05-11T08:26:20Z DEBUG Traceback (most recent call last):
>>>    File "/usr/lib/python2.7/site-packages/ipaserver/install/service.py", 
>>> line
>>> 447, in start_creation
>>>      run_step(full_msg, method)
>>>    File "/usr/lib/python2.7/site-packages/ipaserver/install/service.py", 
>>> line
>>> 437, in run_step
>>>      method()
>>>    File
>>> "/usr/lib/python2.7/site-packages/ipaserver/install/upgradeinstance.py", 
>>> line
>>> 221, in __upgrade
>>>      raise RuntimeError(e)
>>> RuntimeError: 'BindInstance' object has no attribute
>>> 'named_conf_get_directive'
>>>
>>> PATCH * Add ipaDNSVersion option to dnsconfig* commands and use new
>>> attribute *
>>> 2)
>>> +        Int('ipadnsversion?',
>>> +            label=_('IPA DNS version'),
>>> +        ),
>>>
>>> Shouldn't be this part of System: Read DNS Configuration permission?
>>>
>>> 3)
>>> -    def postprocess_result(self, result):
>>> +    def postprocess_result(self, result, show_version):
>>>           if not any(param in result['result'] for param in self.params):
>>>               result['summary'] = unicode(_('Global DNS configuration is
>>> empty'))
>>>
>>> show_version param was added but I don't see it used in this patch.
>>>
>>> 4)
>>> +        Int('ipadnsversion?',
>>> +            label=_('IPA DNS version'),
>>> +        ),
>>>
>>> Could we add comment here that this option is accessible only from 
>>> installers
>>> and upgrade?
>>>
>>> 5)
>>> +        for config_option in container_entry.get("ipaConfigString", []):
>>> +            matched = re.match("^DNSVersion\s+(?P<version>\d+)$",
>>> +                               config_option, flags=re.I)
>>> +            if matched:
>>> +                version = int(matched.group("version"))
>>>
>>> Shouldn't we print error if version cannot be parsed?
>>>
>>> PATCH  * DNS upgrade: separate backup logic to make it reusable *
>>>
>>> LGTM
>>>
>>> PATCH * Add function ipapython.dnsutil.related_to_auto_empty_zone() *
>>>
>>> 7)
>>> I'm curious why do you need to check superdomains?
>>>
>>> PATCH * DNS upgrade: change forwarding policy to = only for conflicting
>>> forward zones*
>>>
>>> 8)
>>> +            self.log.debug('Zone %s was sucessfully modified to use '
>>> +                           'forward policy "only"', zone['idnsname'][0])
>>> <---missing empty line---->
>>> +    def execute(self, **options):
>>>
>>> PATCH * DNS upgrade: change global forwarding policy in LDAP to "only" if
>>> private IPs are used *
>>> 9)
>>> - dnsutil.related_to_auto_empty_zone(zone.get('idnsname')[0])
>>> +                dnsutil.related_to_auto_empty_zone(
>>> +                    dnsutil.DNSName(zone.get('idnsname')[0]))
>>>
>>> Should be in previous commit
>>>
>>> 10)
>>> -            return
>>> +            return False, []
>>> This should be fixed in the previous commit
>>>
>>> PATCH * DNS upgrade: change global forwarding policy in named.conf to "only"
>>> if private IPs are used *
>>> 11)
>>> IMO this is an upgrade of configuration and this should be in
>>> ipaserver/install/server/upgrade.py, upgrade plugins are used only for
>>> updating of LDAP values
>>>
>>> Unless you really want to use this as precedence, but then it requires 
>>> broader
>>> discussion.
>>>
>>> 12)
>>>
>>> bind.named_conf_get_directive
>>> should be
>>> bindinstance.named_conf_get_directive
>>>
>>> see 1)
>> This new patchset completely obsoletes the old one. I had to reshuffle few
>> things to to make the split between server config & LDAP upgrade possible.
>>
>> Hopefully I addressed all your comment.
>>
> 
> commits
> * Move IP address resolution from ipaserver.install.installutils to
> ipapython.dnsutil *  and * Turn verify_host_resolvable() into a wrapper around
> ipapython.dnsutil *
> 
> cause regression in case that dns.python resolver returns NoNameservers
> exception, it is handled as 'Internal server error'
> 
> In original code every exception was caught and transformed to
> DNSNotARecordError.
> 
> So we have following options:
> * keep the old behavior in 'resolve_rrsets' and catch all exceptions there
> * or catch all DNS errors in 'verify_host_resolvable' and raise it as new
> PublicError (DNSGenericError (doesn't exist) for example)
> 
> 
> E               InternalError: an internal error has occurred
> 
> ../ipalib/rpc.py:1100: InternalError
>  test_forwardzone_delegation_warnings.test_command[0017: dnsrecord_mod: Delete
> (using dnsrecord-mod) NS record which delegates zone
> u'fw.sub2.sub.dnszone.test.' from zone u'dnszone.test' (expected warning for
> u'fw.sub2.sub.dnszone.test.')]
> 
> [Wed May 25 12:17:00.172143 2016] [wsgi:error] [pid 62789] Traceback (most
> recent call last):
> [Wed May 25 12:17:00.172152 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/ipaserver/rpcserver.py", line 350, in
> wsgi_execute
> [Wed May 25 12:17:00.172158 2016] [wsgi:error] [pid 62789] result =
> self.Command[name](*args, **options)
> [Wed May 25 12:17:00.172164 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/ipalib/frontend.py", line 434, in __call__
> [Wed May 25 12:17:00.172168 2016] [wsgi:error] [pid 62789] return
> self.__do_call(*args, **options)
> [Wed May 25 12:17:00.172173 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/ipalib/frontend.py", line 460, in __do_call
> [Wed May 25 12:17:00.172178 2016] [wsgi:error] [pid 62789]     ret =
> self.run(*args, **options)
> [Wed May 25 12:17:00.172183 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/ipalib/frontend.py", line 777, in run
> [Wed May 25 12:17:00.172189 2016] [wsgi:error] [pid 62789] return
> self.execute(*args, **options)
> [Wed May 25 12:17:00.172194 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/ipalib/plugins/dns.py", line 3774, in 
> execute
> [Wed May 25 12:17:00.172199 2016] [wsgi:error] [pid 62789] result =
> super(dnsrecord_add, self).execute(*keys, **options)
> [Wed May 25 12:17:00.172204 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/ipalib/plugins/baseldap.py", line 1230, in
> execute
> [Wed May 25 12:17:00.172209 2016] [wsgi:error] [pid 62789] *keys, **options)
> [Wed May 25 12:17:00.172213 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/ipalib/plugins/dns.py", line 3719, in
> pre_callback
> [Wed May 25 12:17:00.172229 2016] [wsgi:error] [pid 62789]
> self.obj.run_precallback_validators(dn, entry_attrs, *keys, **options)
> [Wed May 25 12:17:00.172237 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/ipalib/plugins/dns.py", line 3135, in
> run_precallback_validators
> [Wed May 25 12:17:00.172242 2016] [wsgi:error] [pid 62789] rtype_cb(ldap, dn,
> entry_attrs, *keys, **options)
> [Wed May 25 12:17:00.172247 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/ipalib/plugins/dns.py", line 3057, in
> _nsrecord_pre_callback
> [Wed May 25 12:17:00.172252 2016] [wsgi:error] [pid 62789]
> check_ns_rec_resolvable(keys[0], DNSName(nsrecord), self.log)
> [Wed May 25 12:17:00.172256 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/ipalib/plugins/dns.py", line 1577, in
> check_ns_rec_resolvable
> [Wed May 25 12:17:00.172261 2016] [wsgi:error] [pid 62789]
> verify_host_resolvable(name)
> [Wed May 25 12:17:00.172265 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/ipalib/util.py", line 70, in
> verify_host_resolvable
> [Wed May 25 12:17:00.172270 2016] [wsgi:error] [pid 62789]     if not
> resolve_ip_addresses(fqdn):
> [Wed May 25 12:17:00.172274 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/ipapython/dnsutil.py", line 328, in
> resolve_ip_addresses
> [Wed May 25 12:17:00.172278 2016] [wsgi:error] [pid 62789] rrsets =
> resolve_rrsets(fqdn, ['A', 'AAAA'])
> [Wed May 25 12:17:00.172282 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/ipapython/dnsutil.py", line 305, in
> resolve_rrsets
> [Wed May 25 12:17:00.172287 2016] [wsgi:error] [pid 62789] answer =
> dns.resolver.query(fqdn, rdtype)
> [Wed May 25 12:17:00.172292 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/dns/resolver.py", line 1029, in query
> [Wed May 25 12:17:00.172296 2016] [wsgi:error] [pid 62789] raise_on_no_answer,
> source_port)
> [Wed May 25 12:17:00.172301 2016] [wsgi:error] [pid 62789]   File
> "/usr/lib/python2.7/site-packages/dns/resolver.py", line 856, in query
> [Wed May 25 12:17:00.172328 2016] [wsgi:error] [pid 62789]     raise
> NoNameservers(request=request, errors=errors)


Fixed patches are attached.


Please note that I've renumbered the patches because the naming does not match
the original set anymore.

-- 
Petr^2 Spacek
From 91114eba73d433bdc072adc1734d96626b8c993d Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Tue, 17 May 2016 17:06:32 +0200
Subject: [PATCH] Move check_zone_overlap() from ipapython.ipautil to
 ipapython.dnsutil

This is preparatory work to avoid (future) cyclic import between
ipapython.dnsutil and ipapython.ipautil.

https://fedorahosted.org/freeipa/ticket/5710
---
 ipalib/plugins/dns.py              |  3 ++-
 ipapython/dnsutil.py               | 35 +++++++++++++++++++++++++++++++++++
 ipapython/ipautil.py               | 35 -----------------------------------
 ipaserver/install/bindinstance.py  |  7 ++++---
 ipaserver/install/dns.py           |  4 ++--
 ipaserver/install/server/common.py |  2 +-
 6 files changed, 44 insertions(+), 42 deletions(-)

diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py
index fdca0936f985c6a2c30d2e7a2cac333436543ea4..ae8f0fe4fc60c29f2ff91e32786254c4f324efdc 100644
--- a/ipalib/plugins/dns.py
+++ b/ipalib/plugins/dns.py
@@ -64,7 +64,8 @@ from ipalib.util import (normalize_zonemgr,
                          validate_dnssec_zone_forwarder_step2,
                          verify_host_resolvable)
 from ipapython.dn import DN
-from ipapython.ipautil import CheckedIPAddress, check_zone_overlap
+from ipapython.ipautil import CheckedIPAddress
+from ipapython.dnsutil import check_zone_overlap
 from ipapython.dnsutil import DNSName
 
 if six.PY3:
diff --git a/ipapython/dnsutil.py b/ipapython/dnsutil.py
index 240b7c9cd2168db17b9fa673dc4eefcfa78fef45..6287e3eefb683fb124766789eaad6db4c2ce4cf5 100644
--- a/ipapython/dnsutil.py
+++ b/ipapython/dnsutil.py
@@ -19,6 +19,7 @@
 
 import dns.name
 import dns.exception
+import dns.resolver
 import copy
 
 import six
@@ -228,3 +229,37 @@ def inside_auto_empty_zone(name):
         if name.is_subdomain(aez):
             return True
     return False
+
+
+def check_zone_overlap(zone, raise_on_error=True):
+    root_logger.info("Checking DNS domain %s, please wait ..." % zone)
+    if not isinstance(zone, DNSName):
+        zone = DNSName(zone).make_absolute()
+
+    # automatic empty zones always exist so checking them is pointless,
+    # do not report them to avoid meaningless error messages
+    if is_auto_empty_zone(zone):
+        return
+
+    try:
+        containing_zone = dns.resolver.zone_for_name(zone)
+    except dns.exception.DNSException as e:
+        msg = ("DNS check for domain %s failed: %s." % (zone, e))
+        if raise_on_error:
+            raise ValueError(msg)
+        else:
+            root_logger.warning(msg)
+            return
+
+    if containing_zone == zone:
+        try:
+            ns = [ans.to_text() for ans in dns.resolver.query(zone, 'NS')]
+        except dns.exception.DNSException as e:
+            root_logger.debug("Failed to resolve nameserver(s) for domain"
+                              " {0}: {1}".format(zone, e))
+            ns = []
+
+        msg = u"DNS zone {0} already exists in DNS".format(zone)
+        if ns:
+            msg += u" and is handled by server(s): {0}".format(', '.join(ns))
+        raise ValueError(msg)
diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py
index d4c8e8b82ac22d8903f77f1acc8edc8c0e98e458..34e05d36698e58aec0fae8ee9679e904709f2379 100644
--- a/ipapython/ipautil.py
+++ b/ipapython/ipautil.py
@@ -53,7 +53,6 @@ from ipapython.ipa_log_manager import root_logger
 from ipapython import config
 from ipaplatform.paths import paths
 from ipapython.dn import DN
-from ipapython.dnsutil import DNSName, is_auto_empty_zone
 
 SHARE_DIR = paths.USR_SHARE_IPA_DIR
 PLUGINS_SHARE_DIR = paths.IPA_PLUGINS
@@ -1018,40 +1017,6 @@ def reverse_record_exists(ip_address):
     return True
 
 
-def check_zone_overlap(zone, raise_on_error=True):
-    root_logger.info("Checking DNS domain %s, please wait ..." % zone)
-    if not isinstance(zone, DNSName):
-        zone = DNSName(zone).make_absolute()
-
-    # automatic empty zones always exist so checking them is pointless,
-    # do not report them to avoid meaningless error messages
-    if is_auto_empty_zone(zone):
-        return
-
-    try:
-        containing_zone = resolver.zone_for_name(zone)
-    except DNSException as e:
-        msg = ("DNS check for domain %s failed: %s." % (zone, e))
-        if raise_on_error:
-            raise ValueError(msg)
-        else:
-            root_logger.warning(msg)
-            return
-
-    if containing_zone == zone:
-        try:
-            ns = [ans.to_text() for ans in resolver.query(zone, 'NS')]
-        except DNSException as e:
-            root_logger.debug("Failed to resolve nameserver(s) for domain"
-                              " {0}: {1}".format(zone, e))
-            ns = []
-
-        msg = u"DNS zone {0} already exists in DNS".format(zone)
-        if ns:
-            msg += u" and is handled by server(s): {0}".format(', '.join(ns))
-        raise ValueError(msg)
-
-
 def config_replace_variables(filepath, replacevars=dict(), appendvars=dict()):
     """
     Take a key=value based configuration file, and write new version
diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py
index 0cc8d2589be7ae835c807218b5c63ab3cb02a3a7..0a988562fcb3171f5c4db096fe217acb6416b362 100644
--- a/ipaserver/install/bindinstance.py
+++ b/ipaserver/install/bindinstance.py
@@ -34,6 +34,7 @@ from ipaserver.install import installutils
 from ipaserver.install import service
 from ipaserver.install.cainstance import IPA_CA_RECORD
 from ipapython import sysrestore, ipautil, ipaldap
+from ipapython import dnsutil
 from ipapython.ipa_log_manager import root_logger
 from ipapython.dn import DN
 import ipalib
@@ -293,7 +294,7 @@ def read_reverse_zone(default, ip_address, allow_zone_overlap=False):
             continue
         if not allow_zone_overlap:
             try:
-                ipautil.check_zone_overlap(zone, raise_on_error=False)
+                dnsutil.check_zone_overlap(zone, raise_on_error=False)
             except ValueError as e:
                 root_logger.error("Reverse zone %s will not be used: %s"
                                   % (zone, e))
@@ -313,7 +314,7 @@ def get_auto_reverse_zones(ip_addresses):
             continue
         default_reverse = get_reverse_zone_default(ip)
         try:
-            ipautil.check_zone_overlap(default_reverse)
+            dnsutil.check_zone_overlap(default_reverse)
         except ValueError:
             root_logger.info("Reverse zone %s for IP address %s already exists"
                              % (default_reverse, ip))
@@ -460,7 +461,7 @@ def check_reverse_zones(ip_addresses, reverse_zones, options, unattended,
         # isn't the zone managed by someone else
         if not options.allow_zone_overlap:
             try:
-                ipautil.check_zone_overlap(rz)
+                dnsutil.check_zone_overlap(rz)
             except ValueError as e:
                 msg = "Reverse zone %s will not be used: %s" % (rz, e)
                 if unattended:
diff --git a/ipaserver/install/dns.py b/ipaserver/install/dns.py
index ccb7760e3c835d195963e9338e4a5fab8cb608b0..0fb869a7bab5b38f0a543b74220b2bb74888357f 100644
--- a/ipaserver/install/dns.py
+++ b/ipaserver/install/dns.py
@@ -118,7 +118,7 @@ def install_check(standalone, api, replica, options, hostname):
         domain = dnsutil.DNSName(util.normalize_zone(api.env.domain))
         print("Checking DNS domain %s, please wait ..." % domain)
         try:
-            ipautil.check_zone_overlap(domain, raise_on_error=False)
+            dnsutil.check_zone_overlap(domain, raise_on_error=False)
         except ValueError as e:
             if options.force or options.allow_zone_overlap:
                 root_logger.warning("%s Please make sure that the domain is "
@@ -129,7 +129,7 @@ def install_check(standalone, api, replica, options, hostname):
 
     for reverse_zone in options.reverse_zones:
         try:
-            ipautil.check_zone_overlap(reverse_zone)
+            dnsutil.check_zone_overlap(reverse_zone)
         except ValueError as e:
             if options.force or options.allow_zone_overlap:
                 root_logger.warning(e.message)
diff --git a/ipaserver/install/server/common.py b/ipaserver/install/server/common.py
index ecddc6143e42acd317a5a9c8423c0dea87975244..45fb2dc17976a08acab16783584524721411fb4e 100644
--- a/ipaserver/install/server/common.py
+++ b/ipaserver/install/server/common.py
@@ -14,7 +14,7 @@ from ipapython.install import common, core
 from ipapython.install.core import Knob
 from ipalib.util import validate_domain_name
 from ipaserver.install import bindinstance
-from ipapython.ipautil import check_zone_overlap
+from ipapython.dnsutil import check_zone_overlap
 
 if six.PY3:
     unicode = str
-- 
2.5.5

From 3f098a1da46ac2b559777256165748e5514ce647 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Tue, 17 May 2016 17:20:25 +0200
Subject: [PATCH] Use root_logger for verify_host_resolvable()

After discussion with Martin Basti we decided to standardize on root_logger
with hope that one day we will use root_logger.getLogger('module')
to make logging prettier and tunable per module.

https://fedorahosted.org/freeipa/ticket/5710
---
 client/ipa-client-install          | 2 +-
 install/tools/ipa-replica-manage   | 2 +-
 ipalib/plugins/dns.py              | 4 ++--
 ipalib/plugins/host.py             | 2 +-
 ipalib/plugins/service.py          | 8 ++++----
 ipalib/util.py                     | 8 +++++---
 ipaserver/install/bindinstance.py  | 2 +-
 ipatests/test_integration/tasks.py | 2 +-
 8 files changed, 16 insertions(+), 14 deletions(-)

diff --git a/client/ipa-client-install b/client/ipa-client-install
index cff3fbfcdee8690c9466ea339a362edfb151a11a..538b6adee8a22839c5f16d46a242228ca3ca43ee 100755
--- a/client/ipa-client-install
+++ b/client/ipa-client-install
@@ -1753,7 +1753,7 @@ def get_server_connection_interface(server):
 def client_dns(server, hostname, options):
 
     try:
-        verify_host_resolvable(hostname, root_logger)
+        verify_host_resolvable(hostname)
         dns_ok = True
     except errors.DNSNotARecordError:
         root_logger.warning("Hostname (%s) does not have A/AAAA record.",
diff --git a/install/tools/ipa-replica-manage b/install/tools/ipa-replica-manage
index 14e768965601cef08f13792bb5cd086534199538..8e692d295aafdcd6ae8a32b6d125f998434d8adb 100755
--- a/install/tools/ipa-replica-manage
+++ b/install/tools/ipa-replica-manage
@@ -830,7 +830,7 @@ def enforce_host_existence(host, message=None):
         return
 
     try:
-        verify_host_resolvable(host, root_logger)
+        verify_host_resolvable(host)
     except errors.DNSNotARecordError as ex:
         if message is None:
             message = "Unknown host %s: %s" % (host, ex)
diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py
index ae8f0fe4fc60c29f2ff91e32786254c4f324efdc..ddab8ef55e439de07023f9464eeec7d899fd9f1e 100644
--- a/ipalib/plugins/dns.py
+++ b/ipalib/plugins/dns.py
@@ -1656,7 +1656,7 @@ def check_ns_rec_resolvable(zone, name, log):
         # this is a DNS name relative to the zone
         name = name.derelativize(zone.make_absolute())
     try:
-        verify_host_resolvable(name, log)
+        verify_host_resolvable(name)
     except errors.DNSNotARecordError:
         raise errors.NotFound(
             reason=_('Nameserver \'%(host)s\' does not have a corresponding '
@@ -4271,7 +4271,7 @@ class dns_resolve(Command):
         query=args[0]
 
         try:
-            verify_host_resolvable(query, self.log)
+            verify_host_resolvable(query)
         except errors.DNSNotARecordError:
             raise errors.NotFound(
                 reason=_('Host \'%(host)s\' not found') % {'host': query}
diff --git a/ipalib/plugins/host.py b/ipalib/plugins/host.py
index f15d5db2f2a0582f9e04323667ad70a47e14a0f4..4cd08ebb2f507faa3fd193b323ac3d196fc4895d 100644
--- a/ipalib/plugins/host.py
+++ b/ipalib/plugins/host.py
@@ -633,7 +633,7 @@ class host_add(LDAPCreate):
                     check_forward=True,
                     check_reverse=check_reverse)
         if not options.get('force', False) and not 'ip_address' in options:
-            util.verify_host_resolvable(keys[-1], self.log)
+            util.verify_host_resolvable(keys[-1])
         if 'locality' in entry_attrs:
             entry_attrs['l'] = entry_attrs['locality']
         entry_attrs['cn'] = keys[-1]
diff --git a/ipalib/plugins/service.py b/ipalib/plugins/service.py
index 4f03bd35f80805c7f821cac8a3f0e85c547a6219..2d3476e832dc8694eae04a0ecd5c6836300117c9 100644
--- a/ipalib/plugins/service.py
+++ b/ipalib/plugins/service.py
@@ -563,10 +563,10 @@ class service_add(LDAPCreate):
         entry_attrs['usercertificate'] = certs_der
 
         if not options.get('force', False):
-             # We know the host exists if we've gotten this far but we
-             # really want to discourage creating services for hosts that
-             # don't exist in DNS.
-             util.verify_host_resolvable(hostname, self.log)
+            # We know the host exists if we've gotten this far but we
+            # really want to discourage creating services for hosts that
+            # don't exist in DNS.
+            util.verify_host_resolvable(hostname)
         if not 'managedby' in entry_attrs:
             entry_attrs['managedby'] = hostresult['dn']
 
diff --git a/ipalib/util.py b/ipalib/util.py
index 709bfbb8ef90924768823ed4316aacb9a0b9d79b..7d3a502e4b8bbc264310801ca4bd660699833823 100644
--- a/ipalib/util.py
+++ b/ipalib/util.py
@@ -43,6 +43,7 @@ from ipapython.ssh import SSHPublicKey
 from ipapython.dn import DN, RDN
 from ipapython.dnsutil import DNSName
 from ipapython.graph import Graph
+from ipapython.ipa_log_manager import root_logger
 
 if six.PY3:
     unicode = str
@@ -64,7 +65,8 @@ def json_serialize(obj):
         return ''
     return json_serialize(obj.__json__())
 
-def verify_host_resolvable(fqdn, log):
+
+def verify_host_resolvable(fqdn):
     """
     See if the hostname has a DNS A/AAAA record.
     """
@@ -75,12 +77,12 @@ def verify_host_resolvable(fqdn, log):
     for rdtype in ('A', 'AAAA'):
         try:
             answers = resolver.query(fqdn, rdtype)
-            log.debug(
+            root_logger.debug(
                 'IPA: found %d %s records for %s: %s' % (len(answers),
                 rdtype, fqdn, ' '.join(str(answer) for answer in answers))
             )
         except DNSException:
-            log.debug(
+            root_logger.debug(
                 'IPA: DNS %s record lookup failed for %s' %
                 (rdtype, fqdn)
             )
diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py
index 0a988562fcb3171f5c4db096fe217acb6416b362..2a642422ae4a1e689ab5e223e80b155a125b9578 100644
--- a/ipaserver/install/bindinstance.py
+++ b/ipaserver/install/bindinstance.py
@@ -878,7 +878,7 @@ class BindInstance(service.Service):
         if not dns_zone_exists(zone, self.api):
             # check if master hostname is resolvable
             try:
-                verify_host_resolvable(fqdn, root_logger)
+                verify_host_resolvable(fqdn)
             except errors.DNSNotARecordError:
                 root_logger.warning("Master FQDN (%s) is not resolvable.",
                                     fqdn)
diff --git a/ipatests/test_integration/tasks.py b/ipatests/test_integration/tasks.py
index 70f4fa7fd96f9d993332996f087577d1c88f828e..aebd907ebfdd0c513c8c96eece220f009a0e8953 100644
--- a/ipatests/test_integration/tasks.py
+++ b/ipatests/test_integration/tasks.py
@@ -1064,7 +1064,7 @@ def add_a_records_for_hosts_in_master_domain(master):
         # We don't need to take care of the zone creation since it is master
         # domain
         try:
-            verify_host_resolvable(host.hostname, log)
+            verify_host_resolvable(host.hostname)
             log.debug("The host (%s) is resolvable." % host.domain.name)
         except errors.DNSNotARecordError:
             log.debug("Hostname (%s) does not have A/AAAA record. Adding new one.",
-- 
2.5.5

From 19a852ea1a70feff90307533c2eae0a2fe082fed Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Tue, 17 May 2016 17:26:20 +0200
Subject: [PATCH] Move IP address resolution from
 ipaserver.install.installutils to ipapython.dnsutil

This is to make it reusable from other modules and to avoid future code
duplication.

https://fedorahosted.org/freeipa/ticket/5710
---
 ipapython/dnsutil.py              | 59 +++++++++++++++++++++++++++++++++++++++
 ipaserver/install/bindinstance.py | 16 ++++++++---
 ipaserver/install/installutils.py | 26 ++++-------------
 3 files changed, 76 insertions(+), 25 deletions(-)

diff --git a/ipapython/dnsutil.py b/ipapython/dnsutil.py
index 6287e3eefb683fb124766789eaad6db4c2ce4cf5..7fa7646d96e4462d7c08e57928c1bca41f57a411 100644
--- a/ipapython/dnsutil.py
+++ b/ipapython/dnsutil.py
@@ -24,6 +24,9 @@ import copy
 
 import six
 
+from ipapython.ipautil import CheckedIPAddress
+from ipapython.ipa_log_manager import root_logger
+
 if six.PY3:
     unicode = str
 
@@ -231,6 +234,62 @@ def inside_auto_empty_zone(name):
     return False
 
 
+def resolve_rrsets(fqdn, rdtypes):
+    """
+    Get Resource Record sets for given FQDN.
+    CNAME chain is followed during resolution
+    but CNAMEs are not returned in the resulting rrset.
+
+    :returns:
+        set of dns.rrset.RRset objects, can be empty
+        if the FQDN does not exist or if none of rrtypes exist
+    """
+    # empty set of rdtypes would always return empty set of rrsets
+    assert rdtypes, "rdtypes must not be empty"
+
+    if not isinstance(fqdn, DNSName):
+        fqdn = DNSName(fqdn)
+
+    fqdn = fqdn.make_absolute()
+    rrsets = set()
+    for rdtype in rdtypes:
+        try:
+            answer = dns.resolver.query(fqdn, rdtype)
+            root_logger.debug('found %d %s records for %s: %s',
+                              len(answer), rdtype, fqdn, ' '.join(
+                                  str(rr) for rr in answer))
+            rrsets.add(answer.rrset)
+        except dns.resolver.NXDOMAIN as ex:
+            root_logger.debug(ex)
+            break  # no such FQDN, do not iterate
+        except dns.resolver.NoAnswer as ex:
+            root_logger.debug(ex)  # record type does not exist for given FQDN
+        except dns.exception.DNSException as ex:
+            root_logger.error('DNS query for %s %s failed: %s',
+                              fqdn, rdtype, ex)
+            raise
+
+    return rrsets
+
+
+def resolve_ip_addresses(fqdn):
+    """Get IP addresses from DNS A/AAAA records for given host.
+    :returns:
+        list of IP addresses as CheckedIPAddress objects
+    """
+    rrsets = resolve_rrsets(fqdn, ['A', 'AAAA'])
+    ip_addresses = set()
+    for rrset in rrsets:
+        ip_addresses.update({CheckedIPAddress(ip,  # accept whatever is in DNS
+                                              parse_netmask=False,
+                                              allow_network=True,
+                                              allow_loopback=True,
+                                              allow_broadcast=True,
+                                              allow_multicast=True)
+                             for ip in rrset})
+    return ip_addresses
+
+
 def check_zone_overlap(zone, raise_on_error=True):
     root_logger.info("Checking DNS domain %s, please wait ..." % zone)
     if not isinstance(zone, DNSName):
diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py
index 2a642422ae4a1e689ab5e223e80b155a125b9578..ec8526a8e596c352999c450abe6c22ae03e66b4f 100644
--- a/ipaserver/install/bindinstance.py
+++ b/ipaserver/install/bindinstance.py
@@ -908,7 +908,9 @@ class BindInstance(service.Service):
             if fqdn == self.fqdn:
                 continue
 
-            addrs = installutils.resolve_host(fqdn)
+            addrs = dnsutil.resolve_ip_addresses(fqdn)
+            # hack, will go away with locations
+            addrs = [str(addr) for addr in addrs]
 
             root_logger.debug("Adding DNS records for master %s" % fqdn)
             self.__add_master_records(fqdn, addrs)
@@ -964,7 +966,9 @@ class BindInstance(service.Service):
                 if dns_zone_exists(zone, self.api):
                     addrs = get_fwd_rr(zone, host, api=self.api)
                 else:
-                    addrs = installutils.resolve_host(fqdn)
+                    addrs = dnsutil.resolve_ip_addresses(fqdn)
+                    # hack, will go away with locations
+                    addrs = [str(addr) for addr in addrs]
 
                 self.__add_ipa_ca_records(fqdn, addrs, True)
 
@@ -1084,7 +1088,9 @@ class BindInstance(service.Service):
         if dns_zone_exists(zone, self.api):
             addrs = get_fwd_rr(zone, host, api=self.api)
         else:
-            addrs = installutils.resolve_host(fqdn)
+            addrs = dnsutil.resolve_ip_addresses(fqdn)
+            # hack, will go away with locations
+            addrs = [str(addr) for addr in addrs]
 
         self.domain = domain_name
 
@@ -1172,7 +1178,9 @@ class BindInstance(service.Service):
         if dns_zone_exists(zone, self.api):
             addrs = get_fwd_rr(zone, host, api=self.api)
         else:
-            addrs = installutils.resolve_host(fqdn)
+            addrs = dnsutil.resolve_ip_addresses(fqdn)
+            # hack, will go away with locations
+            addrs = [str(addr) for addr in addrs]
 
         for addr in addrs:
             del_fwd_rr(domain_name, IPA_CA_RECORD, addr, api=self.api)
diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index 179909543e8791ff2a85b6bf4ce57dee8d00508b..2a71ef7ac767c8259c6d2bc63399fdec55b3f8dc 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -55,6 +55,7 @@ from ipaserver.install import certs, service, sysupgrade
 from ipaplatform import services
 from ipaplatform.paths import paths
 from ipaplatform.tasks import tasks
+from ipapython import dnsutil
 
 if six.PY3:
     unicode = str
@@ -444,24 +445,6 @@ def create_keytab(path, principal):
 
     kadmin("ktadd -k " + path + " " + principal)
 
-def resolve_host(host_name):
-    try:
-        addrinfos = socket.getaddrinfo(host_name, None,
-                                       socket.AF_UNSPEC, socket.SOCK_STREAM)
-
-        ip_list = []
-
-        for ai in addrinfos:
-            ip = ai[4][0]
-            if ip == "127.0.0.1" or ip == "::1":
-                raise HostnameLocalhost("The hostname resolves to the localhost address")
-
-            ip_list.append(ip)
-
-        return ip_list
-    except socket.error:
-        return []
-
 def get_host_name(no_host_dns):
     """
     Get the current FQDN from the socket and verify that it is valid.
@@ -477,9 +460,10 @@ def get_host_name(no_host_dns):
 
 def get_server_ip_address(host_name, unattended, setup_dns, ip_addresses):
     # Check we have a public IP that is associated with the hostname
-    try:
-        hostaddr = resolve_host(host_name)
-    except HostnameLocalhost:
+    hostaddr = dnsutil.resolve_ip_addresses(host_name)
+    if hostaddr.intersection(
+            {ipautil.CheckedIPAddress(ip, allow_loopback=True)
+             for ip in ['127.0.0.1', '::1']}):
         print("The hostname resolves to the localhost address (127.0.0.1/::1)", file=sys.stderr)
         print("Please change your /etc/hosts file so that the hostname", file=sys.stderr)
         print("resolves to the ip address of your network interface.", file=sys.stderr)
-- 
2.5.5

From 6e947ef87bcf0168e7ff400e1ecc07eb273a8b6f Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Tue, 17 May 2016 17:28:36 +0200
Subject: [PATCH] Turn verify_host_resolvable() into a wrapper around
 ipapython.dnsutil

The code was duplicate and less generic anyway.
As a side-effect I had to re-wrap dns.exception.DNSException into a
PublicError so it can be displayed to the user.

DNSError is now a super class for other DNS-related errors. Errors from
DNS resolver are re-raised as DNSResolverError.

https://fedorahosted.org/freeipa/ticket/5710
---
 ipalib/errors.py | 98 ++++++++++++++++++++++++++++++++++++--------------------
 ipalib/util.py   | 32 ++++--------------
 2 files changed, 71 insertions(+), 59 deletions(-)

diff --git a/ipalib/errors.py b/ipalib/errors.py
index 93333c2aafd0161944f1a49aa9017e1f1906da8a..406a940e58505e59d971756af6ac5e6eda2df7f1 100644
--- a/ipalib/errors.py
+++ b/ipalib/errors.py
@@ -93,7 +93,9 @@ current block assignments:
 
             - **4300 - 4399**  `CertificateError` and its subclasses
 
-            - **4400 - 4999**  *Reserved for future use*
+            - **4400 - 4499**  `DNSError` and (some of) its subclasses
+
+            - **4500 - 4999**  *Reserved for future use*
 
         - **5000 - 5999**  `GenericError` and its subclasses
 
@@ -1164,21 +1166,6 @@ class DefaultGroupError(ExecutionError):
     errno = 4018
     format = _('The default users group cannot be removed')
 
-class DNSNotARecordError(ExecutionError):
-    """
-    **4019** Raised when a hostname is not a DNS A/AAAA record
-
-    For example:
-
-    >>> raise DNSNotARecordError()
-    Traceback (most recent call last):
-      ...
-    DNSNotARecordError: Host does not have corresponding DNS A/AAAA record
-
-    """
-
-    errno = 4019
-    format = _('Host does not have corresponding DNS A/AAAA record')
 
 class ManagedGroupError(ExecutionError):
     """
@@ -1598,24 +1585,6 @@ class DatabaseTimeout(DatabaseError):
     format = _('LDAP timeout')
 
 
-class DNSDataMismatch(ExecutionError):
-    """
-    **4212** Raised when an DNS query didn't return expected answer
-    in a configured time limit.
-
-    For example:
-
-    >>> raise DNSDataMismatch(expected="zone3.test. 86400 IN A 192.0.2.1", \
-                              got="zone3.test. 86400 IN A 192.168.1.1")
-    Traceback (most recent call last):
-      ...
-    DNSDataMismatch: DNS check failed: Expected {zone3.test. 86400 IN A 192.0.2.1} got {zone3.test. 86400 IN A 192.168.1.1}
-    """
-
-    errno = 4212
-    format = _('DNS check failed: Expected {%(expected)s} got {%(got)s}')
-
-
 class TaskTimeout(DatabaseError):
     """
     **4213** Raised when an LDAP task times out
@@ -1833,6 +1802,67 @@ class CertificateInvalidError(CertificateError):
     format = _('%(name)s certificate is not valid')
 
 
+class DNSError(ExecutionError):
+    """
+    **4400** Base class for DNS execution errors (*4400 - 4499*).
+    These are typically wrapper exceptions around dns.exception.DNSException.
+    """
+
+    errno = 4400
+
+
+class DNSNotARecordError(DNSError):
+    """
+    **4019** Raised when a hostname is not a DNS A/AAAA record
+
+    For example:
+
+    >>> raise DNSNotARecordError(hostname='x')
+    Traceback (most recent call last):
+      ...
+    DNSNotARecordError: Host 'x' does not have corresponding DNS A/AAAA record
+
+    """
+
+    errno = 4019  # this exception was defined before DNSError
+    format = _(
+        'Host \'%(hostname)s\' does not have corresponding DNS A/AAAA record')
+
+
+class DNSDataMismatch(DNSError):
+    """
+    **4212** Raised when an DNS query didn't return expected answer
+    in a configured time limit.
+
+    For example:
+
+    >>> raise DNSDataMismatch(expected="zone3.test. 86400 IN A 192.0.2.1", \
+                              got="zone3.test. 86400 IN A 192.168.1.1")
+    Traceback (most recent call last):
+      ...
+    DNSDataMismatch: DNS check failed: Expected {zone3.test. 86400 IN A 192.0.2.1} got {zone3.test. 86400 IN A 192.168.1.1}
+    """
+
+    errno = 4212  # this exception was defined before DNSError
+    format = _('DNS check failed: Expected {%(expected)s} got {%(got)s}')
+
+
+class DNSResolverError(DNSError):
+    """
+    **4401** Wrapper around dns.exception.DNSException.
+    Raised when an error occured in dns.resolver.
+
+    For example:
+    >>> raise DNSResolverError(exception=ValueError("this is bad"))
+    Traceback (most recent call last):
+      ...
+    DNSResolverError: this is bad
+    """
+
+    errno = 4401
+    format = _('%(exception)s')
+
+
 ##############################################################################
 # 5000 - 5999: Generic errors
 
diff --git a/ipalib/util.py b/ipalib/util.py
index 7d3a502e4b8bbc264310801ca4bd660699833823..3fb46b2986dc5e76ca1fdd5831a147268cc6ccdb 100644
--- a/ipalib/util.py
+++ b/ipalib/util.py
@@ -42,8 +42,8 @@ from ipalib.text import _
 from ipapython.ssh import SSHPublicKey
 from ipapython.dn import DN, RDN
 from ipapython.dnsutil import DNSName
+from ipapython.dnsutil import resolve_ip_addresses
 from ipapython.graph import Graph
-from ipapython.ipa_log_manager import root_logger
 
 if six.PY3:
     unicode = str
@@ -67,30 +67,12 @@ def json_serialize(obj):
 
 
 def verify_host_resolvable(fqdn):
-    """
-    See if the hostname has a DNS A/AAAA record.
-    """
-    if not isinstance(fqdn, DNSName):
-        fqdn = DNSName(fqdn)
-
-    fqdn = fqdn.make_absolute()
-    for rdtype in ('A', 'AAAA'):
-        try:
-            answers = resolver.query(fqdn, rdtype)
-            root_logger.debug(
-                'IPA: found %d %s records for %s: %s' % (len(answers),
-                rdtype, fqdn, ' '.join(str(answer) for answer in answers))
-            )
-        except DNSException:
-            root_logger.debug(
-                'IPA: DNS %s record lookup failed for %s' %
-                (rdtype, fqdn)
-            )
-            continue
-        else:
-            return
-    # dns lookup failed in both tries
-    raise errors.DNSNotARecordError()
+    try:
+        if not resolve_ip_addresses(fqdn):
+            raise errors.DNSNotARecordError(hostname=fqdn)
+    except dns.exception.DNSException as ex:
+        # wrap DNSException in a PublicError
+        raise errors.DNSResolverError(exception=ex)
 
 
 def has_soa_or_ns_record(domain):
-- 
2.5.5

From 5b8a940efeba23a0a2fc183819dc5260a1814abd Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Mon, 25 Apr 2016 14:07:16 +0200
Subject: [PATCH] Add ipaDNSVersion option to dnsconfig* commands and use new
 attribute

Ad-hoc LDAP calls in DNS upgrade code were hard to maintain and
ipaConfigString was bad idea from the very beginning as it was hard to
manipulate the number in it.

To avoid problems in future we are introducing new ipaDNSVersion
attribute which is used on cn=dns instead of ipaConfigString.
Original value of ipaConfigString is kept in the tree for now
so older upgraders see it and do not execute the upgrade procedure again.

The attribute can be changed only by installer/upgrade so it is not
exposed in dnsconfig_mod API.

Command dnsconfig_show displays it only if --all option was used.

https://fedorahosted.org/freeipa/ticket/5710
---
 ACI.txt                                        |  2 +-
 install/share/60ipadns.ldif                    |  2 +
 install/share/dns.ldif                         |  4 +-
 install/updates/40-dns.update                  |  1 -
 install/updates/90-post_upgrade_plugins.update |  1 +
 ipalib/plugins/dns.py                          | 14 ++++-
 ipaserver/install/plugins/dns.py               | 77 ++++++++++++++++++++------
 7 files changed, 78 insertions(+), 23 deletions(-)

diff --git a/ACI.txt b/ACI.txt
index ae00cf7a1b8e2ea0e33798993bb24dc5f06127e3..5ce212a8a9c1734148c6052356bd4edb073595f6 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -51,7 +51,7 @@ aci: (targetattr = "cospriority")(targetfilter = "(objectclass=costemplate)")(ve
 dn: cn=costemplates,cn=accounts,dc=ipa,dc=example
 aci: (targetattr = "cn || cospriority || createtimestamp || entryusn || krbpwdpolicyreference || modifytimestamp || objectclass")(targetfilter = "(objectclass=costemplate)")(version 3.0;acl "permission:System: Read Group Password Policy costemplate";allow (compare,read,search) groupdn = "ldap:///cn=System: Read Group Password Policy costemplate,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: dc=ipa,dc=example
-aci: (targetattr = "createtimestamp || entryusn || idnsallowsyncptr || idnsforwarders || idnsforwardpolicy || idnspersistentsearch || idnszonerefresh || modifytimestamp || objectclass")(target = "ldap:///cn=dns,dc=ipa,dc=example";)(targetfilter = "(objectclass=idnsConfigObject)")(version 3.0;acl "permission:System: Read DNS Configuration";allow (read) groupdn = "ldap:///cn=System: Read DNS Configuration,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+aci: (targetattr = "createtimestamp || entryusn || idnsallowsyncptr || idnsforwarders || idnsforwardpolicy || idnspersistentsearch || idnszonerefresh || ipadnsversion || modifytimestamp || objectclass")(target = "ldap:///cn=dns,dc=ipa,dc=example";)(targetfilter = "(objectclass=idnsConfigObject)")(version 3.0;acl "permission:System: Read DNS Configuration";allow (read) groupdn = "ldap:///cn=System: Read DNS Configuration,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: dc=ipa,dc=example
 aci: (targetattr = "idnsallowsyncptr || idnsforwarders || idnsforwardpolicy || idnspersistentsearch || idnszonerefresh")(target = "ldap:///cn=dns,dc=ipa,dc=example";)(targetfilter = "(objectclass=idnsConfigObject)")(version 3.0;acl "permission:System: Write DNS Configuration";allow (write) groupdn = "ldap:///cn=System: Write DNS Configuration,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: dc=ipa,dc=example
diff --git a/install/share/60ipadns.ldif b/install/share/60ipadns.ldif
index e0ed0ab869cea0478d9640bb509c6267abed1a01..71b99d4d03c34591dc83a5706d300727f3f77f30 100644
--- a/install/share/60ipadns.ldif
+++ b/install/share/60ipadns.ldif
@@ -70,9 +70,11 @@ 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.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' )
 objectClasses: ( 2.16.840.1.113730.3.8.6.0 NAME 'idnsRecord' DESC 'dns Record, usually a host' SUP top STRUCTURAL MUST idnsName MAY ( cn $ idnsAllowDynUpdate $ dNSTTL $ dNSClass $ aRecord $ aAAARecord $ a6Record $ nSRecord $ cNAMERecord $ pTRRecord $ sRVRecord $ tXTRecord $ mXRecord $ mDRecord $ hInfoRecord $ mInfoRecord $ aFSDBRecord $ SigRecord $ KeyRecord $ LocRecord $ nXTRecord $ nAPTRRecord $ kXRecord $ certRecord $ dNameRecord $ dSRecord $ sSHFPRecord $ rRSIGRecord $ nSECRecord $ DLVRecord $ TLSARecord $ UnknownRecord $ RPRecord $ APLRecord $ IPSECKEYRecord $ DHCIDRecord $ HIPRecord $ SPFRecord ) )
 objectClasses: ( 2.16.840.1.113730.3.8.6.1 NAME 'idnsZone' DESC 'Zone class' SUP idnsRecord STRUCTURAL MUST ( idnsZoneActive $ idnsSOAmName $ idnsSOArName $ idnsSOAserial $ idnsSOArefresh $ idnsSOAretry $ idnsSOAexpire $ idnsSOAminimum ) MAY ( idnsUpdatePolicy $ idnsAllowQuery $ idnsAllowTransfer $ idnsAllowSyncPTR $ idnsForwardPolicy $ idnsForwarders $ idnsSecInlineSigning $ nSEC3PARAMRecord ) )
 objectClasses: ( 2.16.840.1.113730.3.8.6.2 NAME 'idnsConfigObject' DESC 'DNS global config options' STRUCTURAL MAY ( idnsForwardPolicy $ idnsForwarders $ idnsAllowSyncPTR $ idnsZoneRefresh $ idnsPersistentSearch ) )
 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.12.36 NAME 'ipaDNSContainer' DESC 'IPA DNS container' AUXILIARY MUST ( ipaDNSVersion ) X-ORIGIN 'IPA v4.3' )
diff --git a/install/share/dns.ldif b/install/share/dns.ldif
index 42b41a8d706a8a3fd826320aff6c9333264128fc..d71e2ad7d7ca530247198d9db71aebd497d0c121 100644
--- a/install/share/dns.ldif
+++ b/install/share/dns.ldif
@@ -2,10 +2,10 @@ dn: cn=dns,$SUFFIX
 changetype: add
 objectClass: idnsConfigObject
 objectClass: nsContainer
-objectClass: ipaConfigObject
+objectClass: ipaDNSContainer
 objectClass: top
 cn: dns
-ipaConfigString: DNSVersion 1
+ipaDNSVersion: 1
 aci: (targetattr = "*")(version 3.0; acl "Allow read access"; allow (read,search,compare) groupdn = "ldap:///cn=Read DNS Entries,cn=permissions,cn=pbac,$SUFFIX" or userattr = "parent[0,1].managedby#GROUPDN";)
 aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Add DNS entries in a zone";allow (add) userattr = "parent[1].managedby#GROUPDN";)
 aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Remove DNS entries from a zone";allow (delete) userattr = "parent[1].managedby#GROUPDN";)
diff --git a/install/updates/40-dns.update b/install/updates/40-dns.update
index 9f64a2f707db5cb0e3503259a0e64d9831ae92f2..4c0824b8350052e47e6f73fc4985cc03e08ee36e 100644
--- a/install/updates/40-dns.update
+++ b/install/updates/40-dns.update
@@ -2,7 +2,6 @@
 # update DNS container
 dn: cn=dns, $SUFFIX
 addifexist: objectClass: idnsConfigObject
-addifexist: objectClass: ipaConfigObject
 addifexist: aci:(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Add DNS entries in a zone";allow (add) userattr = "parent[1].managedby#GROUPDN";)
 addifexist: aci:(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Remove DNS entries from a zone";allow (delete) userattr = "parent[1].managedby#GROUPDN";)
 addifexist: aci:(targetattr = "a6record || aaaarecord || afsdbrecord || aplrecord || arecord || certrecord || cn || cnamerecord || dhcidrecord || dlvrecord || dnamerecord || dnsclass || dnsttl || dsrecord || hinforecord || hiprecord || idnsallowdynupdate || idnsallowquery || idnsallowsyncptr || idnsallowtransfer || idnsforwarders || idnsforwardpolicy || idnsname || idnssecinlinesigning || idnssoaexpire || idnssoaminimum || idnssoamname || idnssoarefresh || idnssoaretry || idnssoarname || idnssoaserial || idnsupdatepolicy || idnszoneactive || ipseckeyrecord || keyrecord || kxrecord || locrecord || mdrecord || minforecord || mxrecord || naptrrecord || nsecrecord || nsec3paramrecord || nsrecord || nxtrecord || ptrrecord || rprecord || rrsigrecord || sigrecord || spfrecord || srvrecord || sshfprecord || tlsarecord || txtrecord || unknownrecord ")(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Update DNS entries in a zone";allow (write) userattr = "parent[0,1].managedby#GROUPDN";)
diff --git a/install/updates/90-post_upgrade_plugins.update b/install/updates/90-post_upgrade_plugins.update
index 9c9ee160fffedbc8e8d59705169e6cf08ddc9779..d8498edd4565fbad91ca5dea48e5bc2296e3fc25 100644
--- a/install/updates/90-post_upgrade_plugins.update
+++ b/install/updates/90-post_upgrade_plugins.update
@@ -3,6 +3,7 @@
 
 # middle
 plugin: update_ca_topology
+plugin: update_ipaconfigstring_dnsversion_to_ipadnsversion
 plugin: update_dnszones
 plugin: update_dns_limits
 plugin: update_sigden_extdom_broken_config
diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py
index ddab8ef55e439de07023f9464eeec7d899fd9f1e..f0a44a862ecda941ab5257b197ecae83b343d692 100644
--- a/ipalib/plugins/dns.py
+++ b/ipalib/plugins/dns.py
@@ -4350,6 +4350,9 @@ class dnsconfig(LDAPObject):
             cli_name='zone_refresh',
             label=_('Zone refresh interval'),
         ),
+        Int('ipadnsversion?',  # available only in installer/upgrade
+            label=_('IPA DNS version'),
+        ),
     )
     managed_permissions = {
         'System: Write DNS Configuration': {
@@ -4376,7 +4379,7 @@ class dnsconfig(LDAPObject):
             'ipapermdefaultattr': {
                 'objectclass',
                 'idnsallowsyncptr', 'idnsforwarders', 'idnsforwardpolicy',
-                'idnspersistentsearch', 'idnszonerefresh'
+                'idnspersistentsearch', 'idnszonerefresh', 'ipadnsversion'
             },
             'default_privileges': {'DNS Administrators', 'DNS Servers'},
         },
@@ -4397,11 +4400,17 @@ class dnsconfig(LDAPObject):
             result['summary'] = unicode(_('Global DNS configuration is empty'))
 
 
-
 @register()
 class dnsconfig_mod(LDAPUpdate):
     __doc__ = _('Modify global DNS configuration.')
 
+    def get_options(self):
+        """hide ipadnsversion outside of installer/upgrade"""
+        for option in super(dnsconfig_mod, self).get_options():
+            if option.name == 'ipadnsversion':
+                option = option.clone(include=('installer', 'updates'))
+            yield option
+
     def interactive_prompt_callback(self, kw):
 
         # show informative message on client side
@@ -4459,6 +4468,7 @@ class dnsconfig_show(LDAPRetrieve):
         return result
 
 
+
 @register()
 class dnsforwardzone(DNSZoneBase):
     """
diff --git a/ipaserver/install/plugins/dns.py b/ipaserver/install/plugins/dns.py
index 7bd2b2697cc7556bc888ebdcf5387da9a563b52d..cb3fad4c1663e6a0033e04a65a8cf98f3499befc 100644
--- a/ipaserver/install/plugins/dns.py
+++ b/ipaserver/install/plugins/dns.py
@@ -32,6 +32,61 @@ from ipapython.ipa_log_manager import root_logger
 register = Registry()
 
 
+class DNSUpdater(Updater):
+    def version_update_needed(self, target_version):
+        """Test if IPA DNS version is smaller than target version."""
+        assert isinstance(target_version, int)
+
+        try:
+            return int(self.api.Command['dnsconfig_show'](
+                all=True)['result']['ipadnsversion'][0]) < target_version
+        except errors.NotFound:
+            # IPA DNS is not configured
+            return False
+
+
+@register()
+class update_ipaconfigstring_dnsversion_to_ipadnsversion(Updater):
+    """
+    IPA <= 4.3.1 used ipaConfigString "DNSVersion 1" on DNS container.
+    This was hard to deal with in API so from IPA 4.3.2 we are using
+    new ipaDNSVersion attribute with integer syntax.
+    Old ipaConfigString is left there for now so if someone accidentally
+    executes upgrade on an old replica again it will not re-upgrade the data.
+    """
+    def execute(self, **options):
+        ldap = self.api.Backend.ldap2
+        dns_container_dn = DN(self.api.env.container_dns, self.api.env.basedn)
+        try:
+            container_entry = ldap.get_entry(dns_container_dn)
+        except errors.NotFound:
+            # DNS container not found, nothing to upgrade
+            return False, []
+
+        if 'ipadnscontainer' in [
+            o.lower() for o in container_entry['objectclass']
+        ]:
+            # version data are already migrated
+            return False, []
+
+        self.log.debug('Migrating DNS ipaConfigString to ipaDNSVersion')
+        container_entry['objectclass'].append('ipadnscontainer')
+        version = 0
+        for config_option in container_entry.get("ipaConfigString", []):
+            matched = re.match("^DNSVersion\s+(?P<version>\d+)$",
+                               config_option, flags=re.I)
+            if matched:
+                version = int(matched.group("version"))
+            else:
+                self.log.error(
+                    'Failed to parse DNS version from ipaConfigString, '
+                    'defaulting to version %s', version)
+        container_entry['ipadnsversion'] = version
+        ldap.update_entry(container_entry)
+        self.log.debug('ipaDNSVersion = %s', version)
+        return False, []
+
+
 @register()
 class update_dnszones(Updater):
     """
@@ -141,7 +196,7 @@ class update_dns_limits(Updater):
 
 
 @register()
-class update_master_to_dnsforwardzones(Updater):
+class update_master_to_dnsforwardzones(DNSUpdater):
     """
     Update all zones to meet requirements in the new FreeIPA versions
 
@@ -158,26 +213,14 @@ class update_master_to_dnsforwardzones(Updater):
     def execute(self, **options):
         ldap = self.api.Backend.ldap2
         # check LDAP if forwardzones already uses new semantics
-        dns_container_dn = DN(self.api.env.container_dns, self.api.env.basedn)
-        try:
-            container_entry = ldap.get_entry(dns_container_dn)
-        except errors.NotFound:
-            # DNS container not found, nothing to upgrade
+        if not self.version_update_needed(target_version=1):
+            # forwardzones already uses new semantics,
+            # no upgrade is required
             return False, []
 
-        for config_option in container_entry.get("ipaConfigString", []):
-            matched = re.match("^DNSVersion\s+(?P<version>\d+)$",
-                               config_option, flags=re.I)
-            if matched and int(matched.group("version")) >= 1:
-                # forwardzones already uses new semantics,
-                # no upgrade is required
-                return False, []
-
         self.log.debug('Updating forward zones')
         # update the DNSVersion, following upgrade can be executed only once
-        container_entry.setdefault(
-            'ipaConfigString', []).append(u"DNSVersion 1")
-        ldap.update_entry(container_entry)
+        self.api.Command['dnsconfig_mod'](ipadnsversion=1)
 
         # Updater in IPA version from 4.0 to 4.1.2 doesn't work well, this
         # should detect if update in past has been executed, and set proper
-- 
2.5.5

From 3d7bebd604b760b768835a4a26cd11b12efd93dd Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Wed, 27 Apr 2016 12:49:23 +0200
Subject: [PATCH] DNS upgrade: separate backup logic to make it reusable

https://fedorahosted.org/freeipa/ticket/5710
---
 ipaserver/install/plugins/dns.py | 145 ++++++++++++++++++++-------------------
 1 file changed, 73 insertions(+), 72 deletions(-)

diff --git a/ipaserver/install/plugins/dns.py b/ipaserver/install/plugins/dns.py
index cb3fad4c1663e6a0033e04a65a8cf98f3499befc..26b2b84b5367c616e1c723c30432c63fbdef5b25 100644
--- a/ipaserver/install/plugins/dns.py
+++ b/ipaserver/install/plugins/dns.py
@@ -33,6 +33,18 @@ register = Registry()
 
 
 class DNSUpdater(Updater):
+    backup_dir = u'/var/lib/ipa/backup/'
+    # override backup_filename in subclass, it will be mangled by strftime
+    backup_filename = None
+
+    def __init__(self, api):
+        super(DNSUpdater, self).__init__(api)
+        backup_path = u'%s%s' % (self.backup_dir, self.backup_filename)
+        self.backup_path = time.strftime(backup_path)
+        self._ldif_writer = None
+        self._saved_privileges = set()  # store privileges only once
+        self.saved_zone_to_privilege = {}
+
     def version_update_needed(self, target_version):
         """Test if IPA DNS version is smaller than target version."""
         assert isinstance(target_version, int)
@@ -44,6 +56,56 @@ class DNSUpdater(Updater):
             # IPA DNS is not configured
             return False
 
+    @property
+    def ldif_writer(self):
+        if not self._ldif_writer:
+            self.log.info('Original zones will be saved in LDIF format in '
+                          '%s file' % self.backup_path)
+            self._ldif_writer = LDIFWriter(open(self.backup_path, 'w'))
+        return self._ldif_writer
+
+    def backup_zone(self, zone):
+        """Backup zone object, its records, permissions, and privileges.
+
+        Mapping from zone to privilege (containing zone's permissions)
+        will be stored in saved_zone_to_privilege dict for further usage.
+        """
+        dn = str(zone['dn'])
+        del zone['dn']  # dn shouldn't be as attribute in ldif
+        self.ldif_writer.unparse(dn, zone)
+
+        ldap = self.api.Backend.ldap2
+        if 'managedBy' in zone:
+            permission = ldap.get_entry(DN(zone['managedBy'][0]))
+            self.ldif_writer.unparse(str(permission.dn), dict(permission.raw))
+            for privilege_dn in permission.get('member', []):
+                # privileges can be shared by multiples zones
+                if privilege_dn not in self._saved_privileges:
+                    self._saved_privileges.add(privilege_dn)
+                    privilege = ldap.get_entry(privilege_dn)
+                    self.ldif_writer.unparse(str(privilege.dn),
+                                             dict(privilege.raw))
+
+            # remember privileges referened by permission
+            if 'member' in permission:
+                self.saved_zone_to_privilege[
+                    zone['idnsname'][0]
+                ] = permission['member']
+
+        if 'idnszone' in zone['objectClass']:
+            # raw values are required to store into ldif
+            records = self.api.Command['dnsrecord_find'](zone['idnsname'][0],
+                                                         all=True,
+                                                         raw=True,
+                                                         sizelimit=0)['result']
+            for record in records:
+                if record['idnsname'][0] == u'@':
+                    # zone record was saved before
+                    continue
+                dn = str(record['dn'])
+                del record['dn']
+                self.ldif_writer.unparse(dn, record)
+
 
 @register()
 class update_ipaconfigstring_dnsversion_to_ipadnsversion(Updater):
@@ -206,9 +268,7 @@ class update_master_to_dnsforwardzones(DNSUpdater):
 
     This should be applied only once, and only if original version was lower than 4.0
     """
-    backup_dir = u'/var/lib/ipa/backup/'
-    backup_filename = u'dns-forward-zones-backup-%Y-%m-%d-%H-%M-%S.ldif'
-    backup_path = u'%s%s' % (backup_dir, backup_filename)
+    backup_filename = u'dns-master-to-forward-zones-%Y-%m-%d-%H-%M-%S.ldif'
 
     def execute(self, **options):
         ldap = self.api.Backend.ldap2
@@ -260,77 +320,18 @@ class update_master_to_dnsforwardzones(DNSUpdater):
             zones_to_transform.append(zone)
 
         if zones_to_transform:
-            # add time to filename
-            self.backup_path = time.strftime(self.backup_path)
-
-            # DNs of privileges which contain dns managed permissions
-            privileges_to_ldif = set()  # store priviledges only once
-            zone_to_privileges = {}  # zone: [privileges cn]
-
             self.log.info('Zones with specified forwarders with policy different'
                           ' than none will be transformed to forward zones.')
-            self.log.info('Original zones will be saved in LDIF format in '
-                          '%s file' % self.backup_path)
-            try:
-
-                with open(self.backup_path, 'w') as f:
-                    writer = LDIFWriter(f)
-                    for zone in zones_to_transform:
-                        # save backup to ldif
-                        try:
-
-                            dn = str(zone['dn'])
-                            del zone['dn']  # dn shouldn't be as attribute in ldif
-                            writer.unparse(dn, zone)
-
-                            if 'managedBy' in zone:
-                                entry = ldap.get_entry(DN(zone['managedBy'][0]))
-                                for privilege_member_dn in entry.get('member', []):
-                                    privileges_to_ldif.add(privilege_member_dn)
-                                writer.unparse(str(entry.dn), dict(entry.raw))
-
-                                # privileges where permission is used
-                                if entry.get('member'):
-                                    zone_to_privileges[zone['idnsname'][0]] = entry['member']
-
-                            # raw values are required to store into ldif
-                            records = self.api.Command['dnsrecord_find'](
-                                        zone['idnsname'][0],
-                                        all=True,
-                                        raw=True,
-                                        sizelimit=0)['result']
-                            for record in records:
-                                if record['idnsname'][0] == u'@':
-                                    # zone record was saved before
-                                    continue
-                                dn = str(record['dn'])
-                                del record['dn']
-                                writer.unparse(dn, record)
-
-                        except Exception as e:
-                            self.log.error('Unable to backup zone %s' %
-                                           zone['idnsname'][0])
-                            self.log.error(traceback.format_exc())
-                            return False, []
-
-                    for privilege_dn in privileges_to_ldif:
-                        try:
-                            entry = ldap.get_entry(privilege_dn)
-                            writer.unparse(str(entry.dn), dict(entry.raw))
-                        except Exception as e:
-                            self.log.error('Unable to backup privilege %s' %
-                                           privilege_dn)
-                            self.log.error(traceback.format_exc())
-                            return False, []
-
-                    f.close()
-            except Exception:
-                self.log.error('Unable to create backup file')
-                self.log.error(traceback.format_exc())
-                return False, []
-
             # update
             for zone in zones_to_transform:
+                try:
+                    self.backup_zone(zone)
+                except Exception:
+                    self.log.error('Unable to create backup for zone, '
+                                   'terminating zone upgrade')
+                    self.log.error(traceback.format_exc())
+                    return False, []
+
                 # delete master zone
                 try:
                     self.api.Command['dnszone_del'](zone['idnsname'])
@@ -374,9 +375,9 @@ class update_master_to_dnsforwardzones(DNSUpdater):
                         continue
 
                     else:
-                        if zone['idnsname'][0] in zone_to_privileges:
+                        if zone['idnsname'][0] in self.saved_zone_to_privilege:
                             privileges = [
-                                dn[0].value for dn in zone_to_privileges[zone['idnsname'][0]]
+                                dn[0].value for dn in self.saved_zone_to_privilege[zone['idnsname'][0]]
                             ]
                             try:
                                 self.api.Command['permission_add_member'](perm_name,
-- 
2.5.5

From 082a4431c97f2464d201d50d50b7f6777cc8f9a6 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Wed, 27 Apr 2016 13:26:48 +0200
Subject: [PATCH] Add function ipapython.dnsutil.related_to_auto_empty_zone()

It allows to test if given DNS name is sub/super domain
of an automatic empty zone.

https://fedorahosted.org/freeipa/ticket/5710
---
 ipapython/dnsutil.py | 30 ++++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)

diff --git a/ipapython/dnsutil.py b/ipapython/dnsutil.py
index 7fa7646d96e4462d7c08e57928c1bca41f57a411..98fcc1bfcb1d745a2f9e1b3165bec04f1d997680 100644
--- a/ipapython/dnsutil.py
+++ b/ipapython/dnsutil.py
@@ -234,6 +234,36 @@ def inside_auto_empty_zone(name):
     return False
 
 
+def related_to_auto_empty_zone(name):
+    """True if specified absolute name is a sub/superdomain of an automatic
+    empty zone.
+
+    DNS domain is a subdomain of itself so this function
+    returns True for zone apexes, too.
+
+    >>> related_to_auto_empty_zone(DNSName('.'))
+    True
+    >>> related_to_auto_empty_zone(DNSName('in-addr.arpa.'))
+    True
+    >>> related_to_auto_empty_zone(DNSName('10.in-addr.arpa.'))
+    True
+    >>> related_to_auto_empty_zone(DNSName('1.10.in-addr.arpa.'))
+    True
+    >>> related_to_auto_empty_zone(DNSName('unrelated.example.'))
+    False
+    >>> related_to_auto_empty_zone(DNSName('1.10.in-addr.arpa'))
+    Traceback (most recent call last):
+      ...
+    AssertionError: ...
+    """
+    assert_absolute_dnsname(name)
+    relations = {dns.name.NAMERELN_SUBDOMAIN,
+                 dns.name.NAMERELN_EQUAL,
+                 dns.name.NAMERELN_SUPERDOMAIN}
+    return any(name.fullcompare(aez)[0] in relations
+               for aez in EMPTY_ZONES)
+
+
 def resolve_rrsets(fqdn, rdtypes):
     """
     Get Resource Record sets for given FQDN.
-- 
2.5.5

From f6f8591cba8d3e21e902b54ef6a91110148e65d6 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Wed, 27 Apr 2016 14:44:17 +0200
Subject: [PATCH] DNS upgrade: change forwarding policy to = only for
 conflicting forward zones

This change is necessary to override automatic empty zone configuration
in latest BIND and bind-dyndb-ldap 9.0+.

This procedure is still not complete because we need to handle global
forwarders too (in LDAP and in named.conf on each server).

https://fedorahosted.org/freeipa/ticket/5710
---
 install/share/dns.ldif                         |  4 +-
 install/updates/90-post_upgrade_plugins.update |  3 +
 ipaserver/install/plugins/dns.py               | 78 ++++++++++++++++++++++++++
 3 files changed, 84 insertions(+), 1 deletion(-)

diff --git a/install/share/dns.ldif b/install/share/dns.ldif
index d71e2ad7d7ca530247198d9db71aebd497d0c121..bd5cc57f90ed66066699af06a74e1426cc8f9a59 100644
--- a/install/share/dns.ldif
+++ b/install/share/dns.ldif
@@ -2,10 +2,12 @@ dn: cn=dns,$SUFFIX
 changetype: add
 objectClass: idnsConfigObject
 objectClass: nsContainer
+objectClass: ipaConfigObject
 objectClass: ipaDNSContainer
 objectClass: top
 cn: dns
-ipaDNSVersion: 1
+ipaConfigString: DNSVersion 1
+ipaDNSVersion: 2
 aci: (targetattr = "*")(version 3.0; acl "Allow read access"; allow (read,search,compare) groupdn = "ldap:///cn=Read DNS Entries,cn=permissions,cn=pbac,$SUFFIX" or userattr = "parent[0,1].managedby#GROUPDN";)
 aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Add DNS entries in a zone";allow (add) userattr = "parent[1].managedby#GROUPDN";)
 aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Remove DNS entries from a zone";allow (delete) userattr = "parent[1].managedby#GROUPDN";)
diff --git a/install/updates/90-post_upgrade_plugins.update b/install/updates/90-post_upgrade_plugins.update
index d8498edd4565fbad91ca5dea48e5bc2296e3fc25..ccb72efa4fa7af7fd0391b489d9c746332426fb9 100644
--- a/install/updates/90-post_upgrade_plugins.update
+++ b/install/updates/90-post_upgrade_plugins.update
@@ -17,7 +17,10 @@ plugin: update_service_principalalias
 plugin: update_upload_cacrt
 
 # last
+# DNS version 1
 plugin: update_master_to_dnsforwardzones
+# DNS version 2
+plugin: update_dnsforward_emptyzones
 plugin: update_managed_post
 plugin: update_managed_permissions
 plugin: update_read_replication_agreements_permission
diff --git a/ipaserver/install/plugins/dns.py b/ipaserver/install/plugins/dns.py
index 26b2b84b5367c616e1c723c30432c63fbdef5b25..f3fed9a189f767864e3d8150a370bfbdb7483572 100644
--- a/ipaserver/install/plugins/dns.py
+++ b/ipaserver/install/plugins/dns.py
@@ -26,6 +26,7 @@ from ldif import LDIFWriter
 from ipalib import Registry, errors, util
 from ipalib import Updater
 from ipapython.dn import DN
+from ipapython import dnsutil
 from ipalib.plugins.dns import dns_container_exists
 from ipapython.ipa_log_manager import root_logger
 
@@ -396,3 +397,80 @@ class update_master_to_dnsforwardzones(DNSUpdater):
                               zone['idnsname'][0])
 
         return False, []
+
+
+@register()
+class update_dnsforward_emptyzones(DNSUpdater):
+    """
+    Migrate forward policies which conflict with automatic empty zones
+    (RFC 6303) to use forward policy = only.
+
+    BIND ignores conflicting forwarding configuration
+    when forwarding policy != only.
+    bind-dyndb-ldap 9.0+ will do the same so we have to adjust FreeIPA zones
+    accordingly.
+    """
+    backup_filename = u'dns-forwarding-empty-zones-%Y-%m-%d-%H-%M-%S.ldif'
+
+    def update_zones(self):
+        try:
+            fwzones = self.api.Command.dnsforwardzone_find(all=True,
+                                                           raw=True)['result']
+        except errors.NotFound:
+            # No forwardzones found, we are done
+            return
+
+        logged_once = False
+        for zone in fwzones:
+            if not (
+                dnsutil.related_to_auto_empty_zone(
+                    dnsutil.DNSName(zone.get('idnsname')[0]))
+                and zone.get('idnsforwardpolicy', [u'first'])[0] != u'only'
+                and zone.get('idnsforwarders', []) != []
+            ):
+                # this zone does not conflict with automatic empty zone
+                continue
+
+            if not logged_once:
+                self.log.info('Forward policy for zones conflicting with '
+                              'automatic empty zones will be changed to '
+                              '"only"')
+                logged_once = True
+
+            # backup
+            try:
+                self.backup_zone(zone)
+            except Exception:
+                self.log.error('Unable to create backup for zone %s, '
+                               'terminating zone upgrade', zone['idnsname'][0])
+                self.log.error(traceback.format_exc())
+                continue
+
+            # change forward policy
+            try:
+                self.api.Command['dnsforwardzone_mod'](
+                    zone['idnsname'][0],
+                    idnsforwardpolicy=u'only'
+                )
+            except Exception as e:
+                self.log.error('Forward policy update for zone %s failed '
+                               '(%s)' % (zone['idnsname'][0], e))
+                self.log.error(traceback.format_exc())
+                continue
+
+            self.log.debug('Zone %s was sucessfully modified to use '
+                           'forward policy "only"', zone['idnsname'][0])
+
+    def execute(self, **options):
+        # check LDAP if DNS subtree already uses new semantics
+        if not self.version_update_needed(target_version=2):
+            # forwardzones already use new semantics, no upgrade is required
+            return False, []
+
+        self.log.debug('Updating forwarding policies to avoid conflicts '
+                       'with automatic empty zones')
+        # update the DNSVersion, following upgrade can be executed only once
+        self.api.Command['dnsconfig_mod'](ipadnsversion=2)
+
+        self.update_zones()
+        return False, []
-- 
2.5.5

From d6cf104e4a777c6f9bc7a785bda096d2289d9684 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Wed, 27 Apr 2016 15:24:01 +0200
Subject: [PATCH] DNS upgrade: change global forwarding policy in LDAP to
 "only" if private IPs are used

This change is necessary to override automatic empty zone configuration
in latest BIND and bind-dyndb-ldap 9.0+.

This procedure is still not complete because we need to handle global
forwarders in named.conf too (independently on each server).

https://fedorahosted.org/freeipa/ticket/5710
---
 ipapython/dnsutil.py             | 18 ++++++++++++++++++
 ipaserver/install/plugins/dns.py | 16 ++++++++++++++++
 2 files changed, 34 insertions(+)

diff --git a/ipapython/dnsutil.py b/ipapython/dnsutil.py
index 98fcc1bfcb1d745a2f9e1b3165bec04f1d997680..f68d3c4105b13e5a8b1338da194612d51aae5331 100644
--- a/ipapython/dnsutil.py
+++ b/ipapython/dnsutil.py
@@ -264,6 +264,24 @@ def related_to_auto_empty_zone(name):
                for aez in EMPTY_ZONES)
 
 
+def has_empty_zone_addresses(hostname):
+    """Detect if given host is using IP address belonging to
+    an automatic empty zone.
+
+    Information from --ip-address option used in installed is lost by
+    the time when upgrade is run. Use IP addresses from DNS as best
+    approximation.
+
+    This is brain-dead and duplicates logic from DNS installer
+    but I did not find other way around.
+    """
+    ip_addresses = resolve_ip_addresses(hostname)
+    return any(
+        inside_auto_empty_zone(DNSName(ip.reverse_dns))
+        for ip in ip_addresses
+    )
+
+
 def resolve_rrsets(fqdn, rdtypes):
     """
     Get Resource Record sets for given FQDN.
diff --git a/ipaserver/install/plugins/dns.py b/ipaserver/install/plugins/dns.py
index f3fed9a189f767864e3d8150a370bfbdb7483572..3c1d4e9a42603a06e47fca6943a6683c1c1ba101 100644
--- a/ipaserver/install/plugins/dns.py
+++ b/ipaserver/install/plugins/dns.py
@@ -461,6 +461,19 @@ class update_dnsforward_emptyzones(DNSUpdater):
             self.log.debug('Zone %s was sucessfully modified to use '
                            'forward policy "only"', zone['idnsname'][0])
 
+    def update_global_ldap_forwarder(self):
+        config = self.api.Command['dnsconfig_show'](all=True,
+                                                    raw=True)['result']
+        if (
+            config.get('idnsforwardpolicy', [u'first'])[0] == u'first'
+            and config.get('idnsforwarders', [])
+        ):
+            self.log.info('Global forward policy in LDAP for all servers will '
+                          'be changed to "only" to avoid conflicts with '
+                          'automatic empty zones')
+            self.backup_zone(config)
+            self.api.Command['dnsconfig_mod'](idnsforwardpolicy=u'only')
+
     def execute(self, **options):
         # check LDAP if DNS subtree already uses new semantics
         if not self.version_update_needed(target_version=2):
@@ -473,4 +486,7 @@ class update_dnsforward_emptyzones(DNSUpdater):
         self.api.Command['dnsconfig_mod'](ipadnsversion=2)
 
         self.update_zones()
+        if dnsutil.has_empty_zone_addresses(self.api.env.host):
+            self.update_global_ldap_forwarder()
+
         return False, []
-- 
2.5.5

From 3038e4e9ca5d87e61b465d37679de440ad7092f4 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Thu, 28 Apr 2016 22:19:03 +0200
Subject: [PATCH] DNS upgrade: change global forwarding policy in named.conf to
 "only" if private IPs are used

This change is necessary to override automatic empty zone configuration
in latest BIND and bind-dyndb-ldap 9.0+.

This upgrade has to be done on each IPA DNS server independently.

https://fedorahosted.org/freeipa/ticket/5710
---
 ipaserver/install/bindinstance.py   |  7 ++++++
 ipaserver/install/plugins/dns.py    |  7 +++---
 ipaserver/install/server/upgrade.py | 46 +++++++++++++++++++++++++++++++++++++
 3 files changed, 57 insertions(+), 3 deletions(-)

diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py
index ec8526a8e596c352999c450abe6c22ae03e66b4f..afcb6b0c10e54b1aae975fd1e81f37144e6b97ed 100644
--- a/ipaserver/install/bindinstance.py
+++ b/ipaserver/install/bindinstance.py
@@ -32,6 +32,7 @@ import six
 
 from ipaserver.install import installutils
 from ipaserver.install import service
+from ipaserver.install import sysupgrade
 from ipaserver.install.cainstance import IPA_CA_RECORD
 from ipapython import sysrestore, ipautil, ipaldap
 from ipapython import dnsutil
@@ -1038,6 +1039,12 @@ class BindInstance(service.Service):
                                      section=NAMED_SECTION_OPTIONS,
                                      str_val=False)
 
+        # prevent repeated upgrade on new installs
+        sysupgrade.set_upgrade_state(
+            'named.conf',
+            'forward_policy_conflict_with_empty_zones_handled', True
+        )
+
     def __setup_resolv_conf(self):
         if not self.fstore.has_file(RESOLV_CONF):
             self.fstore.backup_file(RESOLV_CONF)
diff --git a/ipaserver/install/plugins/dns.py b/ipaserver/install/plugins/dns.py
index 3c1d4e9a42603a06e47fca6943a6683c1c1ba101..d2a9bd8f869dae858dcb2393f344e1937096e70c 100644
--- a/ipaserver/install/plugins/dns.py
+++ b/ipaserver/install/plugins/dns.py
@@ -267,7 +267,8 @@ class update_master_to_dnsforwardzones(DNSUpdater):
     than none, will be tranformed to forward zones.
     Original masters zone will be backed up to ldif file.
 
-    This should be applied only once, and only if original version was lower than 4.0
+    This should be applied only once,
+    and only if original version was lower than 4.0
     """
     backup_filename = u'dns-master-to-forward-zones-%Y-%m-%d-%H-%M-%S.ldif'
 
@@ -480,8 +481,8 @@ class update_dnsforward_emptyzones(DNSUpdater):
             # forwardzones already use new semantics, no upgrade is required
             return False, []
 
-        self.log.debug('Updating forwarding policies to avoid conflicts '
-                       'with automatic empty zones')
+        self.log.debug('Updating forwarding policies in LDAP '
+                       'to avoid conflicts with automatic empty zones')
         # update the DNSVersion, following upgrade can be executed only once
         self.api.Command['dnsconfig_mod'](ipadnsversion=2)
 
diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py
index c16dfd5e74d4776d9d256b7127da96227c08b9e6..2398aea90bb1def6ab1534d3640a6bfa20a6b237 100644
--- a/ipaserver/install/server/upgrade.py
+++ b/ipaserver/install/server/upgrade.py
@@ -24,6 +24,7 @@ from ipapython import ipautil, sysrestore, version, certdb
 from ipapython import ipaldap
 from ipapython.ipa_log_manager import root_logger
 from ipapython import certmonger
+from ipapython import dnsutil
 from ipapython.dn import DN
 from ipaplatform.constants import constants
 from ipaplatform.paths import paths
@@ -776,6 +777,50 @@ def named_root_key_include():
     return True
 
 
+def named_update_global_forwarder_policy():
+    bind = bindinstance.BindInstance()
+    if not bindinstance.named_conf_exists() or not bind.is_configured():
+        # DNS service may not be configured
+        root_logger.info('DNS is not configured')
+        return False
+
+    root_logger.info('[Checking global forwarding policy in named.conf '
+                     'to avoid conflicts with automatic empty zones]')
+    if sysupgrade.get_upgrade_state(
+        'named.conf', 'forward_policy_conflict_with_empty_zones_handled'
+    ):
+        # upgrade was done already
+        return False
+
+    sysupgrade.set_upgrade_state(
+        'named.conf',
+        'forward_policy_conflict_with_empty_zones_handled',
+        True
+    )
+    if not dnsutil.has_empty_zone_addresses(api.env.host):
+        # guess: local server does not have IP addresses from private ranges
+        # so hopefully automatic empty zones are not a problem
+        return False
+
+    if bindinstance.named_conf_get_directive(
+            'forward',
+            section=bindinstance.NAMED_SECTION_OPTIONS,
+            str_val=False
+    ) == 'only':
+        return False
+
+    root_logger.info('Global forward policy in named.conf will '
+                     'be changed to "only" to avoid conflicts with '
+                     'automatic empty zones')
+    bindinstance.named_conf_set_directive(
+        'forward',
+        'only',
+        section=bindinstance.NAMED_SECTION_OPTIONS,
+        str_val=False
+    )
+    return True
+
+
 def certificate_renewal_update(ca, ds, http):
     """
     Update certmonger certificate renewal configuration.
@@ -1607,6 +1652,7 @@ def upgrade_configuration():
                           named_bindkey_file_option(),
                           named_managed_keys_dir_option(),
                           named_root_key_include(),
+                          named_update_global_forwarder_policy(),
                           mask_named_regular(),
                           fix_dyndb_ldap_workdir_permissions(),
                          )
-- 
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