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
This commit is contained in:
Martin Kosek
2012-06-28 16:46:48 +02:00
parent 4879c68d68
commit 9d69db80a3
10 changed files with 145 additions and 51 deletions

View File

@@ -46,4 +46,5 @@ dynamic-db "ipa" {
arg "sasl_user DNS/$FQDN"; arg "sasl_user DNS/$FQDN";
arg "zone_refresh $ZONE_REFRESH"; arg "zone_refresh $ZONE_REFRESH";
arg "psearch $PERSISTENT_SEARCH"; arg "psearch $PERSISTENT_SEARCH";
arg "serial_autoincrement $SERIAL_AUTOINCREMENT";
}; };

View File

@@ -62,6 +62,9 @@ def parse_options():
default=0, type="int", default=0, type="int",
help="When set to non-zero the name server will use DNS zone " help="When set to non-zero the name server will use DNS zone "
"detection based on polling instead of a persistent search") "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", parser.add_option("-U", "--unattended", dest="unattended", action="store_true",
default=False, help="unattended installation never prompts the user") default=False, help="unattended installation never prompts the user")
@@ -85,6 +88,10 @@ def parse_options():
if options.zone_notif: if options.zone_notif:
print >>sys.stderr, "WARNING: --zone-notif option is deprecated and has no effect" 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 return safe_options, options
def main(): def main():
@@ -224,7 +231,8 @@ def main():
bind.setup(api.env.host, ip_address, api.env.realm, api.env.domain, bind.setup(api.env.host, ip_address, api.env.realm, api.env.domain,
dns_forwarders, conf_ntp, reverse_zone, zonemgr=options.zonemgr, dns_forwarders, conf_ntp, reverse_zone, zonemgr=options.zonemgr,
zone_refresh=options.zone_refresh, zone_refresh=options.zone_refresh,
persistent_search=options.persistent_search) persistent_search=options.persistent_search,
serial_autoincrement=options.serial_autoincrement)
bind.create_instance() bind.create_instance()
# Restart http instance to make sure that python-dns has the right resolver # Restart http instance to make sure that python-dns has the right resolver

View File

@@ -210,7 +210,10 @@ def parse_options():
default=False, default=False,
help="Do not use DNS for hostname lookup during installation") 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", 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) parser.add_option_group(dns_group)
uninstall_group = OptionGroup(parser, "uninstall options") uninstall_group = OptionGroup(parser, "uninstall options")
@@ -304,6 +307,10 @@ def parse_options():
elif options.zone_refresh > 0: elif options.zone_refresh > 0:
options.persistent_search = False # mutually exclusive features 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: if options.zone_notif:
print >>sys.stderr, "WARNING: --zone-notif option is deprecated and has no effect" 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, bind.setup(host_name, ip_address, realm_name, domain_name, dns_forwarders,
options.conf_ntp, reverse_zone, zonemgr=options.zonemgr, options.conf_ntp, reverse_zone, zonemgr=options.zonemgr,
zone_refresh=options.zone_refresh, zone_refresh=options.zone_refresh,
persistent_search=options.persistent_search) persistent_search=options.persistent_search,
serial_autoincrement=options.serial_autoincrement)
if options.setup_dns: if options.setup_dns:
api.Backend.ldap2.connect(bind_dn="cn=Directory Manager", bind_pw=dm_password) api.Backend.ldap2.connect(bind_dn="cn=Directory Manager", bind_pw=dm_password)

View File

@@ -302,7 +302,7 @@ def upgrade_httpd_selinux(fstore):
http = httpinstance.HTTPInstance(fstore) http = httpinstance.HTTPInstance(fstore)
http.configure_selinux_for_httpd() 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 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 detection and is also needed for other features (DNSSEC, SOA serial
@@ -322,11 +322,13 @@ def enable_psearch_for_named():
return return
try: try:
psearch = bindinstance.named_conf_get_directive('psearch').lower() psearch = bindinstance.named_conf_get_directive('psearch')
except IOError, e: except IOError, e:
root_logger.debug('Cannot retrieve psearch option from %s: %s', root_logger.debug('Cannot retrieve psearch option from %s: %s',
bindinstance.NAMED_CONF, e) bindinstance.NAMED_CONF, e)
return return
else:
psearch = None if psearch is None else psearch.lower()
if not sysupgrade.get_upgrade_state('named.conf', 'psearch_enabled'): if not sysupgrade.get_upgrade_state('named.conf', 'psearch_enabled'):
if psearch != "yes": if psearch != "yes":
try: try:
@@ -343,7 +345,8 @@ def enable_psearch_for_named():
# make sure number of connections is right # make sure number of connections is right
minimum_connections = 2 minimum_connections = 2
if psearch == 'yes': if psearch == 'yes':
minimum_connections = 3 # serial_autoincrement increased the minimal number of connections to 4
minimum_connections = 4
try: try:
connections = bindinstance.named_conf_get_directive('connections') connections = bindinstance.named_conf_get_directive('connections')
except IOError, e: except IOError, e:
@@ -373,6 +376,59 @@ def enable_psearch_for_named():
root_logger.debug('No changes made') root_logger.debug('No changes made')
return changed 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(): def main():
""" """
Get some basics about the system. If getting those basics fail then Get some basics about the system. If getting those basics fail then
@@ -435,9 +491,11 @@ def main():
cleanup_kdc(fstore) cleanup_kdc(fstore)
upgrade_ipa_profile(krbctx.default_realm) upgrade_ipa_profile(krbctx.default_realm)
changed = enable_psearch_for_named() changed_psearch = named_enable_psearch()
if changed: changed_autoincrement = named_enable_serial_autoincrement()
if changed_psearch or changed_autoincrement:
# configuration has changed, restart the name server # configuration has changed, restart the name server
root_logger.info('Changes to named.conf have been made, restart named')
bindinstance.BindInstance(fstore).restart() bindinstance.BindInstance(fstore).restart()
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -16,7 +16,7 @@
.\" .\"
.\" Author: Rob Crittenden <rcritten@redhat.com> .\" 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" .SH "NAME"
ipa\-dns\-install \- Add DNS as a service to an IPA server ipa\-dns\-install \- Add DNS as a service to an IPA server
.SH "SYNOPSIS" .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 \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. 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 .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 \fB\-U\fR, \fB\-\-unattended\fR
An unattended installation that will never prompt for user input An unattended installation that will never prompt for user input
.SH "EXIT STATUS" .SH "EXIT STATUS"

View File

@@ -16,7 +16,7 @@
.\" .\"
.\" Author: Rob Crittenden <rcritten@redhat.com> .\" 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" .SH "NAME"
ipa\-server\-install \- Configure an IPA server ipa\-server\-install \- Configure an IPA server
.SH "SYNOPSIS" .SH "SYNOPSIS"
@@ -156,6 +156,9 @@ Do not use DNS for hostname lookup during installation
.TP .TP
\fB\-\-no\-dns\-sshfp\fR \fB\-\-no\-dns\-sshfp\fR
Do not automatically create DNS SSHFP records. 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" .SS "UNINSTALL OPTIONS"
.TP .TP

View File

@@ -244,8 +244,15 @@ def _rname_validator(ugettext, zonemgr):
return None return None
def _create_zone_serial(): 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): def _reverse_zone_name(netstr):
net = netaddr.IPNetwork(netstr) net = netaddr.IPNetwork(netstr)

View File

@@ -467,7 +467,7 @@ class BindInstance(service.Service):
def setup(self, fqdn, ip_address, realm_name, domain_name, forwarders, ntp, def setup(self, fqdn, ip_address, realm_name, domain_name, forwarders, ntp,
reverse_zone, named_user="named", zonemgr=None, 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.named_user = named_user
self.fqdn = fqdn self.fqdn = fqdn
self.ip_address = ip_address self.ip_address = ip_address
@@ -480,6 +480,7 @@ class BindInstance(service.Service):
self.reverse_zone = reverse_zone self.reverse_zone = reverse_zone
self.zone_refresh = zone_refresh self.zone_refresh = zone_refresh
self.persistent_search = persistent_search self.persistent_search = persistent_search
self.serial_autoincrement = True
if not zonemgr: if not zonemgr:
self.zonemgr = 'hostmaster.%s' % self.domain 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 optional_ntp += "_ntp._udp\t\tIN SRV 0 100 123\t%s""" % self.host_in_rr
else: else:
optional_ntp = "" 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, self.sub_dict = dict(FQDN=self.fqdn,
IP=self.ip_address, IP=self.ip_address,
@@ -589,7 +593,8 @@ class BindInstance(service.Service):
OPTIONAL_NTP=optional_ntp, OPTIONAL_NTP=optional_ntp,
ZONEMGR=self.zonemgr, ZONEMGR=self.zonemgr,
ZONE_REFRESH=self.zone_refresh, 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): def __setup_dns_container(self):
self._ldap_mod("dns.ldif", self.sub_dict) self._ldap_mod("dns.ldif", self.sub_dict)

View File

@@ -25,28 +25,24 @@ from ipaserver import ipaldap
from ipaserver.install import replication from ipaserver.install import replication
from ipalib import api 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 Run through all replication agreements and ensure that EXCLUDE list
included in the EXCLUDE list so we don't cause replication storms. has all the required attributes so that we don't cause replication
storms.
""" """
order=MIDDLE order=MIDDLE
def execute(self, **options): def execute(self, **options):
totalexcludes = ('entryusn',
'krblastsuccessfulauth',
'krblastfailedauth',
'krbloginfailedcount')
excludes = ('memberof', ) + totalexcludes
# We need an IPAdmin connection to the backend # 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 = ipaldap.IPAdmin(api.env.host, ldapi=True, realm=api.env.realm)
conn.do_external_bind(pwd.getpwuid(os.geteuid()).pw_name) conn.do_external_bind(pwd.getpwuid(os.geteuid()).pw_name)
repl = replication.ReplicationManager(api.env.realm, api.env.host, repl = replication.ReplicationManager(api.env.realm, api.env.host,
None, conn=conn) None, conn=conn)
entries = repl.find_replication_agreements() 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: for replica in entries:
self.log.debug(replica.description) self.log.debug(replica.description)
attrlist = replica.getValue('nsDS5ReplicatedAttributeList') attrlist = replica.getValue('nsDS5ReplicatedAttributeList')
@@ -55,28 +51,33 @@ class update_replica_memberof(PreUpdate):
current = replica.toDict() current = replica.toDict()
# Need to add it altogether # Need to add it altogether
replica.setValues('nsDS5ReplicatedAttributeList', replica.setValues('nsDS5ReplicatedAttributeList',
'(objectclass=*) $ EXCLUDE %s' % " ".join(excludes)) '(objectclass=*) $ EXCLUDE %s' % " ".join(replication.EXCLUDES))
replica.setValues('nsDS5ReplicatedAttributeListTotal', replica.setValues('nsDS5ReplicatedAttributeListTotal',
'(objectclass=*) $ EXCLUDE %s' % " ".join(totalexcludes)) '(objectclass=*) $ EXCLUDE %s' % " ".join(replication.TOTAL_EXCLUDES))
try: try:
repl.conn.updateEntry(replica.dn, current, replica.toDict()) repl.conn.updateEntry(replica.dn, current, replica.toDict())
self.log.debug("Updated") self.log.debug("Updated")
except Exception, e: except Exception, e:
self.log.error("Error caught updating replica: %s" % str(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))
else: 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") self.log.debug("Done updating agreements")
return (False, False, []) # No restart, no apply now, no updates return (False, False, []) # No restart, no apply now, no updates
api.register(update_replica_memberof) api.register(update_replica_exclude_attribute_list)

View File

@@ -43,6 +43,15 @@ REPL_MAN_DN = "cn=replication manager,cn=config"
IPA_REPLICA = 1 IPA_REPLICA = 1
WINSYNC = 2 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, def replica_conn_check(master_host, host_name, realm, check_ca,
admin_password=None): admin_password=None):
""" """
@@ -467,15 +476,6 @@ class ReplicationManager(object):
except errors.NotFound: except errors.NotFound:
pass 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 = ipaldap.Entry(dn)
entry.setValues('objectclass', "nsds5replicationagreement") entry.setValues('objectclass', "nsds5replicationagreement")
entry.setValues('cn', cn) entry.setValues('cn', cn)
@@ -485,7 +485,7 @@ class ReplicationManager(object):
entry.setValues('nsds5replicaroot', self.suffix) entry.setValues('nsds5replicaroot', self.suffix)
if master is None: if master is None:
entry.setValues('nsDS5ReplicatedAttributeList', entry.setValues('nsDS5ReplicatedAttributeList',
'(objectclass=*) $ EXCLUDE %s' % " ".join(excludes)) '(objectclass=*) $ EXCLUDE %s' % " ".join(EXCLUDES))
entry.setValues('description', "me to %s" % b_hostname) entry.setValues('description', "me to %s" % b_hostname)
if isgssapi: if isgssapi:
entry.setValues('nsds5replicatransportinfo', 'LDAP') entry.setValues('nsds5replicatransportinfo', 'LDAP')
@@ -503,7 +503,7 @@ class ReplicationManager(object):
try: try:
mod = [(ldap.MOD_ADD, 'nsDS5ReplicatedAttributeListTotal', mod = [(ldap.MOD_ADD, 'nsDS5ReplicatedAttributeListTotal',
'(objectclass=*) $ EXCLUDE %s' % " ".join(totalexcludes))] '(objectclass=*) $ EXCLUDE %s' % " ".join(TOTAL_EXCLUDES))]
a_conn.modify_s(dn, mod) a_conn.modify_s(dn, mod)
except ldap.LDAPError, e: except ldap.LDAPError, e:
# Apparently there are problems set the total list # Apparently there are problems set the total list