dns: Check if domain already exists.

Raise an error when the domain already exists. This can be overriden using
--force or --allow-zone-overlap options.

https://fedorahosted.org/freeipa/ticket/3681

Reviewed-By: Petr Spacek <pspacek@redhat.com>
This commit is contained in:
David Kupka 2015-12-02 14:20:50 +00:00 committed by Tomas Babej
parent 6c107d819c
commit 8d19da49c4
7 changed files with 189 additions and 59 deletions

View File

@ -57,6 +57,9 @@ def parse_options():
help="The reverse DNS zone to use. This option can be used multiple times") help="The reverse DNS zone to use. This option can be used multiple times")
parser.add_option("--no-reverse", dest="no_reverse", action="store_true", parser.add_option("--no-reverse", dest="no_reverse", action="store_true",
default=False, help="Do not create new reverse DNS zone") default=False, help="Do not create new reverse DNS zone")
parser.add_option("--allow-zone-overlap", dest="allow_zone_overlap",
action="store_true", default=False, help="Create DNS "
"zone even if it already exists")
parser.add_option("--no-dnssec-validation", dest="no_dnssec_validation", action="store_true", parser.add_option("--no-dnssec-validation", dest="no_dnssec_validation", action="store_true",
default=False, help="Disable DNSSEC validation") default=False, help="Disable DNSSEC validation")
parser.add_option("--dnssec-master", dest="dnssec_master", action="store_true", parser.add_option("--dnssec-master", dest="dnssec_master", action="store_true",

View File

@ -62,6 +62,9 @@ Copy OpenDNSSEC metadata from the specified kasp.db file. This will not create a
\fB\-\-zonemgr\fR \fB\-\-zonemgr\fR
The e\-mail address of the DNS zone manager. Defaults to hostmaster@DOMAIN The e\-mail address of the DNS zone manager. Defaults to hostmaster@DOMAIN
.TP .TP
\fB\-\-allow\-zone\-overlap\fR
Allow creatin of (reverse) zone even if the zone is already resolvable. Using this option is discouraged as it result in later problems with domain name resolution.
.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 "DEPRECATED OPTIONS" .SH "DEPRECATED OPTIONS"

View File

@ -175,6 +175,9 @@ Do not automatically create DNS SSHFP records.
.TP .TP
\fB\-\-no\-dnssec\-validation\fR \fB\-\-no\-dnssec\-validation\fR
Disable DNSSEC validation on this server. Disable DNSSEC validation on this server.
.TP
\fB\-\-allow\-zone\-overlap\fR
Allow creatin of (reverse) zone even if the zone is already resolvable. Using this option is discouraged as it result in later problems with domain name resolution.
.SS "UNINSTALL OPTIONS" .SS "UNINSTALL OPTIONS"
.TP .TP

View File

@ -40,7 +40,7 @@ from contextlib import contextmanager
import locale import locale
import collections import collections
from dns import resolver, rdatatype from dns import resolver, rdatatype, reversename
from dns.exception import DNSException, Timeout from dns.exception import DNSException, Timeout
import six import six
from six.moves import input from six.moves import input
@ -1031,6 +1031,22 @@ def host_exists(host):
return True return True
def reverse_record_exists(ip_address):
"""
Checks if IP address have some reverse record somewhere.
Does not care where it points.
Returns True/False
"""
reverse = reversename.from_address(str(ip_address))
try:
resolver.query(reverse, "PTR")
except DNSException:
# really don't care what exception, PTR is simply unresolvable
return False
return True
def check_zone_overlap(zone, raise_on_timeout=True): def check_zone_overlap(zone, raise_on_timeout=True):
root_logger.info("Checking DNS domain %s, please wait ..." % zone) root_logger.info("Checking DNS domain %s, please wait ..." % zone)
if not isinstance(zone, DNSName): if not isinstance(zone, DNSName):

View File

@ -43,10 +43,12 @@ from ipaplatform.constants import constants
from ipaplatform.paths import paths from ipaplatform.paths import paths
from ipaplatform.tasks import tasks from ipaplatform.tasks import tasks
from ipalib.util import (validate_zonemgr_str, normalize_zonemgr, from ipalib.util import (validate_zonemgr_str, normalize_zonemgr,
get_dns_forward_zone_update_policy, get_dns_reverse_zone_update_policy, get_dns_forward_zone_update_policy,
normalize_zone, get_reverse_zone_default, zone_is_reverse, get_dns_reverse_zone_update_policy,
validate_dnssec_global_forwarder, DNSSECSignatureMissingError, normalize_zone, get_reverse_zone_default,
EDNS0UnsupportedError, UnresolvableRecordError) zone_is_reverse, validate_dnssec_global_forwarder,
DNSSECSignatureMissingError, EDNS0UnsupportedError,
UnresolvableRecordError, verify_host_resolvable)
from ipalib.constants import CACERT from ipalib.constants import CACERT
if six.PY3: if six.PY3:
@ -278,20 +280,48 @@ def find_reverse_zone(ip_address, api=api):
return None return None
def read_reverse_zone(default, ip_address): def read_reverse_zone(default, ip_address, allow_zone_overlap=False):
while True: while True:
zone = ipautil.user_input("Please specify the reverse zone name", default=default) zone = ipautil.user_input("Please specify the reverse zone name", default=default)
if not zone: if not zone:
return None return None
if verify_reverse_zone(zone, ip_address): if not verify_reverse_zone(zone, ip_address):
break root_logger.error("Invalid reverse zone %s for IP address %s"
else: % (zone, ip_address))
print("Invalid reverse zone %s for IP address %s" % (zone, ip_address)) continue
if not allow_zone_overlap:
try:
ipautil.check_zone_overlap(zone, raise_on_timeout=False)
except ValueError as e:
root_logger.error("Reverse zone %s will not be used: %s"
% (zone, e))
continue
break
return normalize_zone(zone) return normalize_zone(zone)
def get_auto_reverse_zones(ip_addresses):
auto_zones = []
for ip in ip_addresses:
if ipautil.reverse_record_exists(ip):
# PTR exist there is no reason to create reverse zone
root_logger.info("Reverse record for IP address %s already "
"exists" % ip)
continue
default_reverse = get_reverse_zone_default(ip)
try:
ipautil.check_zone_overlap(default_reverse)
except ValueError:
root_logger.info("Reverse zone %s for IP address %s already exists"
% (default_reverse, ip))
continue
auto_zones.append((ip, default_reverse))
return auto_zones
def add_zone(name, zonemgr=None, dns_backup=None, ns_hostname=None, def add_zone(name, zonemgr=None, dns_backup=None, ns_hostname=None,
update_policy=None, force=False, api=api): update_policy=None, force=False, skip_overlap_check=False,
api=api):
# always normalize zones # always normalize zones
name = normalize_zone(name) name = normalize_zone(name)
@ -317,6 +347,7 @@ def add_zone(name, zonemgr=None, dns_backup=None, ns_hostname=None,
idnsupdatepolicy=unicode(update_policy), idnsupdatepolicy=unicode(update_policy),
idnsallowquery=u'any', idnsallowquery=u'any',
idnsallowtransfer=u'none', idnsallowtransfer=u'none',
skip_overlap_check=skip_overlap_check,
force=force) force=force)
except (errors.DuplicateEntry, errors.EmptyModlist): except (errors.DuplicateEntry, errors.EmptyModlist):
pass pass
@ -406,46 +437,62 @@ def zonemgr_callback(option, opt_str, value, parser):
parser.values.zonemgr = value parser.values.zonemgr = value
def check_reverse_zones(ip_addresses, reverse_zones, options, unattended, search_reverse_zones=False):
reverse_asked = False
ret_reverse_zones = [] def check_reverse_zones(ip_addresses, reverse_zones, options, unattended,
# check that there is IP address in every reverse zone search_reverse_zones=False):
if reverse_zones: checked_reverse_zones = []
for rz in reverse_zones:
for ip in ip_addresses: if not options.no_reverse and not reverse_zones:
if verify_reverse_zone(rz, ip): if unattended:
ret_reverse_zones.append(normalize_zone(rz)) options.no_reverse = True
break else:
else: options.no_reverse = not create_reverse()
# no ip matching reverse zone found
sys.exit("There is no IP address matching reverse zone %s." % rz) # shortcut
if not options.no_reverse: if options.no_reverse:
# check that there is reverse zone for every IP return []
for ip in ip_addresses:
if search_reverse_zones and find_reverse_zone(str(ip)): # verify zones passed in options
# reverse zone is already in LDAP for rz in reverse_zones:
# isn't the zone managed by someone else
if not options.allow_zone_overlap:
try:
ipautil.check_zone_overlap(rz)
except ValueError as e:
msg = "Reverse zone %s will not be used: %s" % (rz, e)
if options.unattended:
sys.exit(msg)
else:
root_logger.warning(msg)
continue continue
for rz in ret_reverse_zones: checked_reverse_zones.append(normalize_zone(rz))
if verify_reverse_zone(rz, ip):
# reverse zone was entered by user # check that there is reverse zone for every IP
break ips_missing_reverse = []
else: for ip in ip_addresses:
# no reverse zone for ip found if search_reverse_zones and find_reverse_zone(str(ip)):
if not reverse_asked: # reverse zone is already in LDAP
if not unattended and not reverse_zones: continue
# user did not specify reverse_zone nor no_reverse for rz in checked_reverse_zones:
options.no_reverse = not create_reverse() if verify_reverse_zone(rz, ip):
if options.no_reverse: # reverse zone was entered by user
# user decided not to create reverse zone break
return [] else:
reverse_asked = True ips_missing_reverse.append(ip)
rz = get_reverse_zone_default(str(ip))
if not unattended: # create reverse zone for IP addresses that does not have one
rz = read_reverse_zone(rz, str(ip)) for (ip, rz) in get_auto_reverse_zones(ips_missing_reverse):
ret_reverse_zones.append(rz) if unattended:
root_logger.warning("Missing reverse record for IP address %s"
% ip)
else:
if ipautil.user_input("Do you want to create reverse zone for IP "
"%s" % ip, True):
rz = read_reverse_zone(rz, str(ip), options.allow_zone_overlap)
checked_reverse_zones.append(rz)
return checked_reverse_zones
return ret_reverse_zones
def check_forwarders(dns_forwarders, logger): def check_forwarders(dns_forwarders, logger):
print("Checking DNS forwarders, please wait ...") print("Checking DNS forwarders, please wait ...")
@ -770,7 +817,8 @@ class BindInstance(service.Service):
def __setup_zone(self): def __setup_zone(self):
# Always use force=True as named is not set up yet # Always use force=True as named is not set up yet
add_zone(self.domain, self.zonemgr, dns_backup=self.dns_backup, add_zone(self.domain, self.zonemgr, dns_backup=self.dns_backup,
ns_hostname=self.api.env.host, force=True, api=self.api) ns_hostname=self.api.env.host, force=True,
skip_overlap_check=True, api=self.api)
add_rr(self.domain, "_kerberos", "TXT", self.realm, api=self.api) add_rr(self.domain, "_kerberos", "TXT", self.realm, api=self.api)
@ -788,7 +836,8 @@ class BindInstance(service.Service):
# Always use force=True as named is not set up yet # Always use force=True as named is not set up yet
for reverse_zone in self.reverse_zones: for reverse_zone in self.reverse_zones:
add_zone(reverse_zone, self.zonemgr, ns_hostname=self.api.env.host, add_zone(reverse_zone, self.zonemgr, ns_hostname=self.api.env.host,
dns_backup=self.dns_backup, force=True, api=self.api) dns_backup=self.dns_backup, force=True,
skip_overlap_check=True, api=self.api)
def __add_master_records(self, fqdn, addrs): def __add_master_records(self, fqdn, addrs):
host, zone = fqdn.split(".", 1) host, zone = fqdn.split(".", 1)
@ -817,18 +866,19 @@ class BindInstance(service.Service):
api=self.api) api=self.api)
if not dns_zone_exists(zone, self.api): if not dns_zone_exists(zone, self.api):
# add DNS domain for host first # check if master hostname is resolvable
root_logger.debug( try:
"Host domain (%s) is different from DNS domain (%s)!" % ( verify_host_resolvable(fqdn, root_logger)
zone, self.domain)) except errors.DNSNotARecordError:
root_logger.debug("Add DNS zone for host first.") root_logger.warning("Master FQDN (%s) is not resolvable.",
fqdn)
add_zone(zone, self.zonemgr, dns_backup=self.dns_backup,
ns_hostname=self.fqdn, force=True, api=self.api)
# Add forward and reverse records to self # Add forward and reverse records to self
for addr in addrs: for addr in addrs:
add_fwd_rr(zone, host, addr, api=self.api) try:
add_fwd_rr(zone, host, addr, self.api)
except errors.NotFound as e:
pass
reverse_zone = find_reverse_zone(addr, self.api) reverse_zone = find_reverse_zone(addr, self.api)
if reverse_zone: if reverse_zone:

View File

@ -13,11 +13,13 @@ from subprocess import CalledProcessError
from ipalib import api from ipalib import api
from ipalib import errors from ipalib import errors
from ipalib import util
from ipaplatform.paths import paths from ipaplatform.paths import paths
from ipaplatform.constants import constants from ipaplatform.constants import constants
from ipaplatform import services from ipaplatform import services
from ipapython import ipautil from ipapython import ipautil
from ipapython import sysrestore from ipapython import sysrestore
from ipapython import dnsutil
from ipapython.dn import DN from ipapython.dn import DN
from ipapython.ipa_log_manager import root_logger from ipapython.ipa_log_manager import root_logger
from ipapython.ipaldap import AUTOBIND_ENABLED from ipapython.ipaldap import AUTOBIND_ENABLED
@ -97,6 +99,19 @@ def _disable_dnssec():
conn.update_entry(entry) conn.update_entry(entry)
def check_dns_enabled(api):
try:
api.Backend.rpcclient.connect()
result = api.Backend.rpcclient.forward(
'dns_is_enabled',
version=u'2.112', # All the way back to 3.0 servers
)
return result['result']
finally:
if api.Backend.rpcclient.isconnected():
api.Backend.rpcclient.disconnect()
def install_check(standalone, replica, options, hostname): def install_check(standalone, replica, options, hostname):
global ip_addresses global ip_addresses
global reverse_zones global reverse_zones
@ -106,6 +121,27 @@ def install_check(standalone, replica, options, hostname):
raise RuntimeError("Integrated DNS requires '%s' package" % raise RuntimeError("Integrated DNS requires '%s' package" %
constants.IPA_DNS_PACKAGE_NAME) constants.IPA_DNS_PACKAGE_NAME)
# when installing first replica with DNS we need to check zone overlap
if not replica or not check_dns_enabled(api):
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_timeout=False)
except ValueError as e:
if options.force or options.allow_zone_overlap:
root_logger.warning(e.message)
else:
raise e
for reverse_zone in options.reverse_zones:
try:
ipautil.check_zone_overlap(reverse_zone)
except ValueError as e:
if options.force or options.allow_zone_overlap:
root_logger.warning(e.message)
else:
raise e
if standalone: if standalone:
print("==============================================================================") print("==============================================================================")
print("This program will setup DNS for the FreeIPA Server.") print("This program will setup DNS for the FreeIPA Server.")

View File

@ -10,6 +10,8 @@ from ipapython.install import common, core
from ipapython.install.core import Knob from ipapython.install.core import Knob
from ipalib.util import validate_domain_name from ipalib.util import validate_domain_name
from ipaserver.install import bindinstance from ipaserver.install import bindinstance
from ipapython.ipautil import check_zone_overlap
from ipapython.dnsutil import DNSName
VALID_SUBJECT_ATTRS = ['st', 'o', 'ou', 'dnqualifier', 'c', VALID_SUBJECT_ATTRS = ['st', 'o', 'ou', 'dnqualifier', 'c',
'serialnumber', 'l', 'title', 'sn', 'givenname', 'serialnumber', 'l', 'title', 'sn', 'givenname',
@ -171,6 +173,11 @@ class BaseServerDNS(common.Installable, core.Group, core.Composite):
description="Do not add any DNS forwarders, use root servers instead", description="Do not add any DNS forwarders, use root servers instead",
) )
allow_zone_overlap = Knob(
bool, False,
description="Create DNS zone even if it already exists",
)
reverse_zones = Knob( reverse_zones = Knob(
(list, str), [], (list, str), [],
description=("The reverse DNS zone to use. This option can be used " description=("The reverse DNS zone to use. This option can be used "
@ -179,6 +186,12 @@ class BaseServerDNS(common.Installable, core.Group, core.Composite):
cli_metavar='REVERSE_ZONE', cli_metavar='REVERSE_ZONE',
) )
@reverse_zones.validator
def reverse_zones(self, values):
if not self.allow_zone_overlap:
for zone in values:
check_zone_overlap(zone)
no_reverse = Knob( no_reverse = Knob(
bool, False, bool, False,
description="Do not create new reverse DNS zone", description="Do not create new reverse DNS zone",
@ -255,6 +268,11 @@ class BaseServer(common.Installable, common.Interactive, core.Composite):
@domain_name.validator @domain_name.validator
def domain_name(self, value): def domain_name(self, value):
validate_domain_name(value) validate_domain_name(value)
if (self.setup_dns and
not self.dns.allow_zone_overlap): # pylint: disable=no-member
print("Checking DNS domain %s, please wait ..." % value)
check_zone_overlap(value, False)
dm_password = Knob( dm_password = Knob(
str, None, str, None,
@ -452,6 +470,7 @@ class BaseServer(common.Installable, common.Interactive, core.Composite):
self.no_forwarders = self.dns.no_forwarders self.no_forwarders = self.dns.no_forwarders
self.reverse_zones = self.dns.reverse_zones self.reverse_zones = self.dns.reverse_zones
self.no_reverse = self.dns.no_reverse self.no_reverse = self.dns.no_reverse
self.allow_zone_overlap = self.dns.allow_zone_overlap
self.no_dnssec_validation = self.dns.no_dnssec_validation self.no_dnssec_validation = self.dns.no_dnssec_validation
self.dnssec_master = self.dns.dnssec_master self.dnssec_master = self.dns.dnssec_master
self.disable_dnssec_master = self.dns.disable_dnssec_master self.disable_dnssec_master = self.dns.disable_dnssec_master