Blob Blame History Raw
From 9d69db80a3d1fc46236a4546988176cdd7939b82 Mon Sep 17 00:00:00 2001
From: Martin Kosek <mkosek@redhat.com>
Date: Thu, 28 Jun 2012 16:46:48 +0200
Subject: [PATCH 23/79] Enable SOA serial autoincrement

SOA serial autoincrement is a requirement for major DNS features,
e.g. zone transfers or DNSSEC. Enable it by default in named.conf
both for new and upgraded installations. Name of the bind-dyndb-ldap
option is "serial_autoincrement".

From now on, idnsSOAserial attribute also has to be put to
replication agreement exclude list as serial will be incremented
on each DNS server separately and won't be shared. Exclude list
has to be updated both for new replication agreements and the
current ones.

Minimum number of connections for bind-dyndb-ldap has been rised
to 4 connections, the setting will be updated during package upgrade.

https://fedorahosted.org/freeipa/ticket/2554
---
 install/share/bind.named.conf.template            |  1 +
 install/tools/ipa-dns-install                     | 10 +++-
 install/tools/ipa-server-install                  | 12 +++-
 install/tools/ipa-upgradeconfig                   | 68 +++++++++++++++++++++--
 install/tools/man/ipa-dns-install.1               |  5 +-
 install/tools/man/ipa-server-install.1            |  5 +-
 ipalib/plugins/dns.py                             | 11 +++-
 ipaserver/install/bindinstance.py                 | 11 +++-
 ipaserver/install/plugins/fix_replica_memberof.py | 51 ++++++++---------
 ipaserver/install/replication.py                  | 22 ++++----
 10 files changed, 145 insertions(+), 51 deletions(-)

diff --git a/install/share/bind.named.conf.template b/install/share/bind.named.conf.template
index f133b089a9eb428e9ad76b66a3ff162b45e5a779..9fdd91319947f6cfd3034f8d2a4fe8bb60d1af77 100644
--- a/install/share/bind.named.conf.template
+++ b/install/share/bind.named.conf.template
@@ -46,4 +46,5 @@ dynamic-db "ipa" {
 	arg "sasl_user DNS/$FQDN";
 	arg "zone_refresh $ZONE_REFRESH";
 	arg "psearch $PERSISTENT_SEARCH";
+	arg "serial_autoincrement $SERIAL_AUTOINCREMENT";
 };
diff --git a/install/tools/ipa-dns-install b/install/tools/ipa-dns-install
index 6e9b9989792aba6f7607348da4693cf605dc0b76..47bffdf8354fa509d64af0ba0e15d5880010e425 100755
--- a/install/tools/ipa-dns-install
+++ b/install/tools/ipa-dns-install
@@ -62,6 +62,9 @@ def parse_options():
                       default=0, type="int",
                       help="When set to non-zero the name server will use DNS zone "
                            "detection based on polling instead of a persistent search")
+    parser.add_option("--no-serial-autoincrement", dest="serial_autoincrement",
+                      default=True, action="store_false",
+                      help="Do not enable SOA serial autoincrement")
     parser.add_option("-U", "--unattended", dest="unattended", action="store_true",
                       default=False, help="unattended installation never prompts the user")
 
@@ -85,6 +88,10 @@ def parse_options():
     if options.zone_notif:
         print >>sys.stderr, "WARNING: --zone-notif option is deprecated and has no effect"
 
+    if options.serial_autoincrement and not options.persistent_search:
+        parser.error('persistent search feature is required for '
+                     'DNS SOA serial autoincrement')
+
     return safe_options, options
 
 def main():
@@ -224,7 +231,8 @@ def main():
     bind.setup(api.env.host, ip_address, api.env.realm, api.env.domain,
                dns_forwarders, conf_ntp, reverse_zone, zonemgr=options.zonemgr,
                zone_refresh=options.zone_refresh,
-               persistent_search=options.persistent_search)
+               persistent_search=options.persistent_search,
+               serial_autoincrement=options.serial_autoincrement)
     bind.create_instance()
 
     # Restart http instance to make sure that python-dns has the right resolver
diff --git a/install/tools/ipa-server-install b/install/tools/ipa-server-install
index 6dc02f684e0b6fc21f150eadef1c286f2a326233..d7de7063111936550ae1f14cf93ebfd46754c829 100755
--- a/install/tools/ipa-server-install
+++ b/install/tools/ipa-server-install
@@ -210,7 +210,10 @@ def parse_options():
                       default=False,
                       help="Do not use DNS for hostname lookup during installation")
     dns_group.add_option("--no-dns-sshfp", dest="create_sshfp", default=True, action="store_false",
-                      help="do not automatically create DNS SSHFP records")
+                      help="Do not automatically create DNS SSHFP records")
+    dns_group.add_option("--no-serial-autoincrement", dest="serial_autoincrement",
+                      default=True, action="store_false",
+                      help="Do not enable SOA serial autoincrement")
     parser.add_option_group(dns_group)
 
     uninstall_group = OptionGroup(parser, "uninstall options")
@@ -304,6 +307,10 @@ def parse_options():
     elif options.zone_refresh > 0:
         options.persistent_search = False   # mutually exclusive features
 
+    if options.serial_autoincrement and not options.persistent_search:
+        parser.error('persistent search feature is required for '
+                     'DNS SOA serial autoincrement')
+
     if options.zone_notif:
         print >>sys.stderr, "WARNING: --zone-notif option is deprecated and has no effect"
 
@@ -1036,7 +1043,8 @@ def main():
     bind.setup(host_name, ip_address, realm_name, domain_name, dns_forwarders,
                options.conf_ntp, reverse_zone, zonemgr=options.zonemgr,
                zone_refresh=options.zone_refresh,
-               persistent_search=options.persistent_search)
+               persistent_search=options.persistent_search,
+               serial_autoincrement=options.serial_autoincrement)
     if options.setup_dns:
         api.Backend.ldap2.connect(bind_dn="cn=Directory Manager", bind_pw=dm_password)
 
diff --git a/install/tools/ipa-upgradeconfig b/install/tools/ipa-upgradeconfig
index 248232ac6e8048b6091c56a7824025f39a275fba..b82f039d4ce8db05ce41193212951d4d79fca79b 100644
--- a/install/tools/ipa-upgradeconfig
+++ b/install/tools/ipa-upgradeconfig
@@ -302,7 +302,7 @@ def upgrade_httpd_selinux(fstore):
     http = httpinstance.HTTPInstance(fstore)
     http.configure_selinux_for_httpd()
 
-def enable_psearch_for_named():
+def named_enable_psearch():
     """
     From IPA 3.0, persistent search is a preferred mechanism for new DNS zone
     detection and is also needed for other features (DNSSEC, SOA serial
@@ -322,11 +322,13 @@ def enable_psearch_for_named():
         return
 
     try:
-        psearch = bindinstance.named_conf_get_directive('psearch').lower()
+        psearch = bindinstance.named_conf_get_directive('psearch')
     except IOError, e:
         root_logger.debug('Cannot retrieve psearch option from %s: %s',
                 bindinstance.NAMED_CONF, e)
         return
+    else:
+        psearch = None if psearch is None else psearch.lower()
     if not sysupgrade.get_upgrade_state('named.conf', 'psearch_enabled'):
         if psearch != "yes":
             try:
@@ -343,7 +345,8 @@ def enable_psearch_for_named():
     # make sure number of connections is right
     minimum_connections = 2
     if psearch == 'yes':
-        minimum_connections = 3
+        # serial_autoincrement increased the minimal number of connections to 4
+        minimum_connections = 4
     try:
         connections = bindinstance.named_conf_get_directive('connections')
     except IOError, e:
@@ -373,6 +376,59 @@ def enable_psearch_for_named():
         root_logger.debug('No changes made')
     return changed
 
+def named_enable_serial_autoincrement():
+    """
+    Serial autoincrement is a requirement for zone transfers or DNSSEC. It
+    should be enabled both for new installs and upgraded servers.
+
+    When some change in named.conf is done, this functions returns True
+    """
+    changed = False
+
+    root_logger.info('[Enabling serial autoincrement in DNS]')
+
+    if not bindinstance.named_conf_exists():
+        # DNS service may not be configured
+        root_logger.debug('DNS not configured')
+        return changed
+
+    try:
+        psearch = bindinstance.named_conf_get_directive('psearch')
+        serial_autoincrement = bindinstance.named_conf_get_directive(
+                                    'serial_autoincrement')
+    except IOError, e:
+        root_logger.debug('Cannot retrieve psearch option from %s: %s',
+                bindinstance.NAMED_CONF, e)
+        return changed
+    else:
+        psearch = None if psearch is None else psearch.lower()
+        serial_autoincrement = None if serial_autoincrement is None \
+                else serial_autoincrement.lower()
+
+    # enable SOA serial autoincrement
+    if not sysupgrade.get_upgrade_state('named.conf', 'autoincrement_enabled'):
+        if psearch != "yes":    # psearch is required
+            root_logger.debug('Persistent search is disabled, '
+                    'serial autoincrement cannot be enabled')
+        else:
+            if serial_autoincrement != 'yes':
+                try:
+                    bindinstance.named_conf_set_directive('serial_autoincrement', 'yes')
+                except IOError, e:
+                    root_logger.error('Cannot enable serial_autoincrement in %s: %s',
+                            bindinstance.NAMED_CONF, e)
+                    return changed
+                else:
+                    root_logger.debug('Serial autoincrement enabled')
+                    changed = True
+            else:
+                root_logger.debug('Serial autoincrement is alredy enabled')
+        sysupgrade.set_upgrade_state('named.conf', 'autoincrement_enabled', True)
+    else:
+        root_logger.debug('Skip serial autoincrement check')
+
+    return changed
+
 def main():
     """
     Get some basics about the system. If getting those basics fail then
@@ -435,9 +491,11 @@ def main():
 
     cleanup_kdc(fstore)
     upgrade_ipa_profile(krbctx.default_realm)
-    changed = enable_psearch_for_named()
-    if changed:
+    changed_psearch = named_enable_psearch()
+    changed_autoincrement = named_enable_serial_autoincrement()
+    if changed_psearch or changed_autoincrement:
         # configuration has changed, restart the name server
+        root_logger.info('Changes to named.conf have been made, restart named')
         bindinstance.BindInstance(fstore).restart()
 
 if __name__ == '__main__':
diff --git a/install/tools/man/ipa-dns-install.1 b/install/tools/man/ipa-dns-install.1
index 9fe89ed1ea267cde7216e9e28e95cfea2349172b..b0bdca94f4aea4a17fecc3362a92a9885bbafed0 100644
--- a/install/tools/man/ipa-dns-install.1
+++ b/install/tools/man/ipa-dns-install.1
@@ -16,7 +16,7 @@
 .\"
 .\" Author: Rob Crittenden <rcritten@redhat.com>
 .\"
-.TH "ipa-dns-install" "1" "Sep 9, 2010" "FreeIPA" "FreeIPA Manual Pages"
+.TH "ipa-dns-install" "1" "Jun 28, 2012" "FreeIPA" "FreeIPA Manual Pages"
 .SH "NAME"
 ipa\-dns\-install \- Add DNS as a service to an IPA server
 .SH "SYNOPSIS"
@@ -55,6 +55,9 @@ Do not enable persistent search mechanism for updating the list of DNS zones in
 \fB\-\-zone\-refresh=\fIZONE_REFRESH\fR
 When set to non-zero value, persistent search zone update mechanism will be disabled and the name server will use a polling mechanism to load new DNS zones every \fIZONE_REFRESH\fR seconds.
 .TP
+\fB\-\-no\-serial\-autoincrement\fR
+Do not enable SOA serial autoincrement feature. SOA serial will have to be updated automatically or other DNS features like zone transfer od DNSSEC will not function properly. This feature requires persistent search zone update mechanism.
+.TP
 \fB\-U\fR, \fB\-\-unattended\fR
 An unattended installation that will never prompt for user input
 .SH "EXIT STATUS"
diff --git a/install/tools/man/ipa-server-install.1 b/install/tools/man/ipa-server-install.1
index 77d40685059fe74bed8d0693c38d2904909c01f3..61b7c669ca26d5fecbdbd9e30fccad4c1d4d9701 100644
--- a/install/tools/man/ipa-server-install.1
+++ b/install/tools/man/ipa-server-install.1
@@ -16,7 +16,7 @@
 .\"
 .\" Author: Rob Crittenden <rcritten@redhat.com>
 .\"
-.TH "ipa-server-install" "1" "Sep 5 2011" "FreeIPA" "FreeIPA Manual Pages"
+.TH "ipa-server-install" "1" "Jun 28 2012" "FreeIPA" "FreeIPA Manual Pages"
 .SH "NAME"
 ipa\-server\-install \- Configure an IPA server
 .SH "SYNOPSIS"
@@ -156,6 +156,9 @@ Do not use DNS for hostname lookup during installation
 .TP
 \fB\-\-no\-dns\-sshfp\fR
 Do not automatically create DNS SSHFP records.
+.TP
+\fB\-\-no\-serial\-autoincrement\fR
+Do not enable SOA serial autoincrement feature. SOA serial will have to be updated automatically or other DNS features like zone transfer od DNSSEC will not function properly. This feature requires persistent search zone update mechanism.
 
 .SS "UNINSTALL OPTIONS"
 .TP
diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py
index c2bf13a2ff5e1c05cb21d07ce9d63e9881ef0da2..857814917f9ce75886aee571885a3dd718427ef3 100644
--- a/ipalib/plugins/dns.py
+++ b/ipalib/plugins/dns.py
@@ -244,8 +244,15 @@ def _rname_validator(ugettext, zonemgr):
     return None
 
 def _create_zone_serial():
-    """ Generate serial number for zones. The format follows RFC 1912 """
-    return int('%s01' % time.strftime('%Y%m%d'))
+    """
+    Generate serial number for zones. bind-dyndb-ldap expects unix time in
+    to be used for SOA serial.
+
+    SOA serial in a date format would also work, but it may be set to far
+    future when many DNS updates are done per day (more than 100). Unix
+    timestamp is more resilient to this issue.
+    """
+    return int(time.time())
 
 def _reverse_zone_name(netstr):
     net = netaddr.IPNetwork(netstr)
diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py
index 3ff593298979534cad79fa35f2626991db82cec7..9faf17698d3cb806ed601f728c3870bfe28c9424 100644
--- a/ipaserver/install/bindinstance.py
+++ b/ipaserver/install/bindinstance.py
@@ -467,7 +467,7 @@ class BindInstance(service.Service):
 
     def setup(self, fqdn, ip_address, realm_name, domain_name, forwarders, ntp,
               reverse_zone, named_user="named", zonemgr=None,
-              zone_refresh=0, persistent_search=True):
+              zone_refresh=0, persistent_search=True, serial_autoincrement=True):
         self.named_user = named_user
         self.fqdn = fqdn
         self.ip_address = ip_address
@@ -480,6 +480,7 @@ class BindInstance(service.Service):
         self.reverse_zone = reverse_zone
         self.zone_refresh = zone_refresh
         self.persistent_search = persistent_search
+        self.serial_autoincrement = True
 
         if not zonemgr:
             self.zonemgr = 'hostmaster.%s' % self.domain
@@ -576,7 +577,10 @@ class BindInstance(service.Service):
             optional_ntp += "_ntp._udp\t\tIN SRV 0 100 123\t%s""" % self.host_in_rr
         else:
             optional_ntp = ""
-        persistent_search = "yes" if self.persistent_search else "no"
+
+        boolean_var = {}
+        for var in ('persistent_search', 'serial_autoincrement'):
+            boolean_var[var] = "yes" if getattr(self, var, False) else "no"
 
         self.sub_dict = dict(FQDN=self.fqdn,
                              IP=self.ip_address,
@@ -589,7 +593,8 @@ class BindInstance(service.Service):
                              OPTIONAL_NTP=optional_ntp,
                              ZONEMGR=self.zonemgr,
                              ZONE_REFRESH=self.zone_refresh,
-                             PERSISTENT_SEARCH=persistent_search)
+                             PERSISTENT_SEARCH=boolean_var['persistent_search'],
+                             SERIAL_AUTOINCREMENT=boolean_var['serial_autoincrement'],)
 
     def __setup_dns_container(self):
         self._ldap_mod("dns.ldif", self.sub_dict)
diff --git a/ipaserver/install/plugins/fix_replica_memberof.py b/ipaserver/install/plugins/fix_replica_memberof.py
index 04152d36021f7d962b335a7553861a13ba03a769..23bde0c9f8b0991e702c27315ebda5e0d3b88424 100644
--- a/ipaserver/install/plugins/fix_replica_memberof.py
+++ b/ipaserver/install/plugins/fix_replica_memberof.py
@@ -25,28 +25,24 @@ from ipaserver import ipaldap
 from ipaserver.install import replication
 from ipalib import api
 
-class update_replica_memberof(PreUpdate):
+class update_replica_exclude_attribute_list(PreUpdate):
     """
-    Run through all replication agreements and ensure that memberOf is
-    included in the EXCLUDE list so we don't cause replication storms.
+    Run through all replication agreements and ensure that EXCLUDE list
+    has all the required attributes so that we don't cause replication
+    storms.
     """
     order=MIDDLE
 
     def execute(self, **options):
-        totalexcludes = ('entryusn',
-                         'krblastsuccessfulauth',
-                         'krblastfailedauth',
-                         'krbloginfailedcount')
-        excludes = ('memberof', ) + totalexcludes
-
         # We need an IPAdmin connection to the backend
+        self.log.debug("Start replication agreement exclude list update task")
         conn = ipaldap.IPAdmin(api.env.host, ldapi=True, realm=api.env.realm)
         conn.do_external_bind(pwd.getpwuid(os.geteuid()).pw_name)
 
         repl = replication.ReplicationManager(api.env.realm, api.env.host,
                                               None, conn=conn)
         entries = repl.find_replication_agreements()
-        self.log.debug("Found %d agreement(s)" % len(entries))
+        self.log.debug("Found %d agreement(s)", len(entries))
         for replica in entries:
             self.log.debug(replica.description)
             attrlist = replica.getValue('nsDS5ReplicatedAttributeList')
@@ -55,28 +51,33 @@ class update_replica_memberof(PreUpdate):
                 current = replica.toDict()
                 # Need to add it altogether
                 replica.setValues('nsDS5ReplicatedAttributeList',
-                    '(objectclass=*) $ EXCLUDE %s' % " ".join(excludes))
+                    '(objectclass=*) $ EXCLUDE %s' % " ".join(replication.EXCLUDES))
                 replica.setValues('nsDS5ReplicatedAttributeListTotal',
-                    '(objectclass=*) $ EXCLUDE %s' % " ".join(totalexcludes))
+                    '(objectclass=*) $ EXCLUDE %s' % " ".join(replication.TOTAL_EXCLUDES))
                 try:
                     repl.conn.updateEntry(replica.dn, current, replica.toDict())
                     self.log.debug("Updated")
                 except Exception, e:
-                    self.log.error("Error caught updating replica: %s" % str(e))
-            elif 'memberof' not in attrlist.lower():
-                self.log.debug("Attribute list needs updating")
-                current = replica.toDict()
-                replica.setValue('nsDS5ReplicatedAttributeList',
-                    replica.nsDS5ReplicatedAttributeList + ' memberof')
-                try:
-                    repl.conn.updateEntry(replica.dn, current, replica.toDict())
-                    self.log.debug("Updated")
-                except Exception, e:
-                    self.log.error("Error caught updating replica: %s" % str(e))
+                    self.log.error("Error caught updating replica: %s", str(e))
             else:
-                self.log.debug("No update necessary")
+                attrlist_normalized = attrlist.lower()
+                missing = [attr for attr in replication.EXCLUDES
+                                if attr not in attrlist_normalized]
+
+                if missing:
+                    self.log.debug("Attribute list needs updating")
+                    current = replica.toDict()
+                    replica.setValue('nsDS5ReplicatedAttributeList',
+                        replica.nsDS5ReplicatedAttributeList + ' %s' % ' '.join(missing))
+                    try:
+                        repl.conn.updateEntry(replica.dn, current, replica.toDict())
+                        self.log.debug("Updated")
+                    except Exception, e:
+                        self.log.error("Error caught updating replica: %s", str(e))
+                else:
+                    self.log.debug("No update necessary")
         self.log.debug("Done updating agreements")
 
         return (False, False, []) # No restart, no apply now, no updates
 
-api.register(update_replica_memberof)
+api.register(update_replica_exclude_attribute_list)
diff --git a/ipaserver/install/replication.py b/ipaserver/install/replication.py
index 417b7a0c5ee29615d2479842efc6862e39a7c3df..38abfe21071e99b0dcc6dce1e6862912361e56e8 100644
--- a/ipaserver/install/replication.py
+++ b/ipaserver/install/replication.py
@@ -43,6 +43,15 @@ REPL_MAN_DN = "cn=replication manager,cn=config"
 IPA_REPLICA = 1
 WINSYNC = 2
 
+# List of attributes that need to be excluded from replication initialization.
+TOTAL_EXCLUDES = ('entryusn',
+                 'krblastsuccessfulauth',
+                 'krblastfailedauth',
+                 'krbloginfailedcount')
+
+# List of attributes that need to be excluded from normal replication.
+EXCLUDES = ('memberof', 'idnssoaserial') + TOTAL_EXCLUDES
+
 def replica_conn_check(master_host, host_name, realm, check_ca,
                        admin_password=None):
     """
@@ -467,15 +476,6 @@ class ReplicationManager(object):
         except errors.NotFound:
             pass
 
-        # List of attributes that need to be excluded from replication initialization.
-        totalexcludes = ('entryusn',
-                         'krblastsuccessfulauth',
-                         'krblastfailedauth',
-                         'krbloginfailedcount')
-
-        # List of attributes that need to be excluded from normal replication.
-        excludes = ('memberof', ) + totalexcludes
-
         entry = ipaldap.Entry(dn)
         entry.setValues('objectclass', "nsds5replicationagreement")
         entry.setValues('cn', cn)
@@ -485,7 +485,7 @@ class ReplicationManager(object):
         entry.setValues('nsds5replicaroot', self.suffix)
         if master is None:
             entry.setValues('nsDS5ReplicatedAttributeList',
-                            '(objectclass=*) $ EXCLUDE %s' % " ".join(excludes))
+                            '(objectclass=*) $ EXCLUDE %s' % " ".join(EXCLUDES))
         entry.setValues('description', "me to %s" % b_hostname)
         if isgssapi:
             entry.setValues('nsds5replicatransportinfo', 'LDAP')
@@ -503,7 +503,7 @@ class ReplicationManager(object):
 
         try:
             mod = [(ldap.MOD_ADD, 'nsDS5ReplicatedAttributeListTotal',
-                   '(objectclass=*) $ EXCLUDE %s' % " ".join(totalexcludes))]
+                   '(objectclass=*) $ EXCLUDE %s' % " ".join(TOTAL_EXCLUDES))]
             a_conn.modify_s(dn, mod)
         except ldap.LDAPError, e:
             # Apparently there are problems set the total list
-- 
1.7.11.2