freeipa/ipaserver/dns_data_management.py

586 lines
20 KiB
Python
Raw Normal View History

#
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
#
from __future__ import absolute_import
import logging
import six
from collections import defaultdict, OrderedDict
from dns import (
rdata,
rdataclass,
rdatatype,
zone,
)
from time import sleep, time
from ipalib import errors
from ipalib.constants import IPA_CA_RECORD
from ipalib.dns import record_name_format
from ipapython.dnsutil import DNSName
from ipaserver.install import installutils
if six.PY3:
unicode=str
logger = logging.getLogger(__name__)
IPA_DEFAULT_MASTER_SRV_REC = (
# srv record name, port
(DNSName('_ldap._tcp'), 389),
# Kerberos records are provided for MIT KRB5 < 1.15 and AD
(DNSName('_kerberos._tcp'), 88),
(DNSName('_kerberos._udp'), 88),
(DNSName('_kerberos-master._tcp'), 88),
(DNSName('_kerberos-master._udp'), 88),
(DNSName('_kpasswd._tcp'), 464),
(DNSName('_kpasswd._udp'), 464),
)
IPA_DEFAULT_MASTER_URI_REC = (
# URI record name, URI template
# MIT KRB5 1.15+ prefers URI records for service discovery
# scheme: always krb5srv
# flags: empty or 'm' for primary server
# transport: 'tcp', 'udp', or 'kkdcp')
# residual: 'hostname', 'hostname:port', or 'https://' URL
(DNSName('_kerberos'), "krb5srv:m:tcp:{hostname}"),
(DNSName('_kerberos'), "krb5srv:m:udp:{hostname}"),
(DNSName('_kpasswd'), "krb5srv:m:tcp:{hostname}"),
(DNSName('_kpasswd'), "krb5srv:m:udp:{hostname}"),
)
IPA_DEFAULT_ADTRUST_SRV_REC = (
# srv record name, port
(DNSName('_ldap._tcp.Default-First-Site-Name._sites.dc._msdcs'), 389),
(DNSName('_ldap._tcp.dc._msdcs'), 389),
(DNSName('_kerberos._tcp.Default-First-Site-Name._sites.dc._msdcs'), 88),
(DNSName('_kerberos._udp.Default-First-Site-Name._sites.dc._msdcs'), 88),
(DNSName('_kerberos._tcp.dc._msdcs'), 88),
(DNSName('_kerberos._udp.dc._msdcs'), 88),
)
IPA_DEFAULT_NTP_SRV_REC = (
# srv record name, port
(DNSName("_ntp._udp"), 123),
)
IPA_DEFAULT_KRB_TXT_REC = (
(DNSName('_kerberos'), "\"{realm}\""),
)
CA_RECORDS_DNS_TIMEOUT = 15 # timeout in seconds
class IPADomainIsNotManagedByIPAError(Exception):
pass
class IPASystemRecords:
# fixme do it configurable
PRIORITY_HIGH = 0
PRIORITY_LOW = 50
# FIXME: use TTL from config
TTL = 3600
def __init__(self, api_instance, all_servers=False):
self.api_instance = api_instance
self.domain_abs = DNSName(self.api_instance.env.domain).make_absolute()
self.servers_data = OrderedDict()
self.__init_data(all_servers=all_servers)
def reload_data(self):
"""
After any change made to IPA servers, this method must be called to
update data in the object, otherwise invalid records may be
created/updated
"""
self.__init_data()
def __get_server_attrs(self, server_result):
weight = int(server_result.get('ipaserviceweight', ['100'])[0])
location = server_result.get('ipalocation_location', [None])[0]
roles = set(server_result.get('enabled_role_servrole', ()))
return weight, location, roles
def __get_location_suffix(self, location):
return location + DNSName('_locations') + self.domain_abs
def __init_data(self, all_servers=False):
self.servers_data.clear()
kwargs = dict(no_members=False)
if not all_servers:
# only active, fully installed masters]
kwargs["servrole"] = "IPA master"
servers = self.api_instance.Command.server_find(**kwargs)
for s in servers['result']:
weight, location, roles = self.__get_server_attrs(s)
self.servers_data[s['cn'][0]] = {
'weight': weight,
'location': location,
'roles': roles,
}
def __add_srv_records(
self, zone_obj, hostname, rname_port_map,
weight=100, priority=0, location=None
):
assert isinstance(hostname, DNSName)
assert isinstance(priority, int)
assert isinstance(weight, int)
if location:
suffix = self.__get_location_suffix(location)
else:
suffix = self.domain_abs
for name, port in rname_port_map:
rd = rdata.from_text(
rdataclass.IN, rdatatype.SRV,
'{0} {1} {2} {3}'.format(
priority, weight, port, hostname.make_absolute()
)
)
r_name = name.derelativize(suffix)
rdataset = zone_obj.get_rdataset(
r_name, rdatatype.SRV, create=True)
rdataset.add(rd, ttl=self.TTL)
def __add_uri_records(
self, zone_obj, hostname, rname_uri_map,
weight=100, priority=0, location=None
):
assert isinstance(hostname, DNSName)
assert isinstance(priority, int)
assert isinstance(weight, int)
if location:
suffix = self.__get_location_suffix(location)
else:
suffix = self.domain_abs
for name, uri_template in rname_uri_map:
uri = uri_template.format(hostname=hostname.make_absolute())
rd = rdata.from_text(
rdataclass.IN, rdatatype.URI,
'{0} {1} {2}'.format(
priority, weight, uri
)
)
r_name = name.derelativize(suffix)
rdataset = zone_obj.get_rdataset(
r_name, rdatatype.URI, create=True)
rdataset.add(rd, ttl=self.TTL)
def __add_ca_records_from_hostname(self, zone_obj, hostname):
assert isinstance(hostname, DNSName) and hostname.is_absolute()
r_name = DNSName(IPA_CA_RECORD) + self.domain_abs
rrsets = None
end_time = time() + CA_RECORDS_DNS_TIMEOUT
while True:
try:
# function logs errors
rrsets = installutils.resolve_rrsets_nss(hostname)
except OSError:
# also retry on EAI_AGAIN, EAI_FAIL
pass
if rrsets:
break
if time() >= end_time:
break
sleep(3)
if not rrsets:
logger.error('unable to resolve host name %s to IP address, '
'ipa-ca DNS record will be incomplete', hostname)
return
for rrset in rrsets:
for rd in rrset:
logger.debug("Adding CA IP %s for %s", rd.to_text(), hostname)
rdataset = zone_obj.get_rdataset(
r_name, rd.rdtype, create=True)
rdataset.add(rd, ttl=self.TTL)
def __add_kerberos_txt_rec(self, zone_obj, location=None):
# FIXME: with external DNS, this should generate records for all
# realmdomains
if location:
suffix = self.__get_location_suffix(location)
else:
suffix = self.domain_abs
r_name = DNSName('_kerberos') + suffix
rd = rdata.from_text(rdataclass.IN, rdatatype.TXT,
self.api_instance.env.realm)
rdataset = zone_obj.get_rdataset(
r_name, rdatatype.TXT, create=True
)
rdataset.add(rd, ttl=self.TTL)
def _add_base_dns_records_for_server(
self, zone_obj, hostname, roles=None, include_master_role=True,
include_kerberos_realm=True,
):
server = self.servers_data[hostname]
if roles:
eff_roles = server['roles'] & set(roles)
else:
eff_roles = server['roles']
hostname_abs = DNSName(hostname).make_absolute()
if include_kerberos_realm:
self.__add_kerberos_txt_rec(zone_obj, location=None)
# get master records
if include_master_role:
self.__add_srv_records(
zone_obj,
hostname_abs,
IPA_DEFAULT_MASTER_SRV_REC,
weight=server['weight']
)
self.__add_uri_records(
zone_obj,
hostname_abs,
IPA_DEFAULT_MASTER_URI_REC,
weight=server['weight']
)
if 'CA server' in eff_roles:
self.__add_ca_records_from_hostname(zone_obj, hostname_abs)
if 'AD trust controller' in eff_roles:
self.__add_srv_records(
zone_obj,
hostname_abs,
IPA_DEFAULT_ADTRUST_SRV_REC,
weight=server['weight']
)
if 'NTP server' in eff_roles:
self.__add_srv_records(
zone_obj,
hostname_abs,
IPA_DEFAULT_NTP_SRV_REC,
weight=server['weight']
)
def _get_location_dns_records_for_server(
self, zone_obj, hostname, locations,
roles=None, include_master_role=True,
include_kerberos_realm=True):
server = self.servers_data[hostname]
if roles:
eff_roles = server['roles'] & roles
else:
eff_roles = server['roles']
hostname_abs = DNSName(hostname).make_absolute()
# generate locations specific records
for location in locations:
if location == self.servers_data[hostname]['location']:
priority = self.PRIORITY_HIGH
else:
priority = self.PRIORITY_LOW
if include_kerberos_realm:
self.__add_kerberos_txt_rec(zone_obj, location)
if include_master_role:
self.__add_srv_records(
zone_obj,
hostname_abs,
IPA_DEFAULT_MASTER_SRV_REC,
weight=server['weight'],
priority=priority,
location=location
)
self.__add_uri_records(
zone_obj,
hostname_abs,
IPA_DEFAULT_MASTER_URI_REC,
weight=server['weight'],
priority=priority,
location=location
)
if 'AD trust controller' in eff_roles:
self.__add_srv_records(
zone_obj,
hostname_abs,
IPA_DEFAULT_ADTRUST_SRV_REC,
weight=server['weight'],
priority=priority,
location=location
)
if 'NTP server' in eff_roles:
self.__add_srv_records(
zone_obj,
hostname_abs,
IPA_DEFAULT_NTP_SRV_REC,
weight=server['weight'],
priority=priority,
location=location
)
return zone_obj
def __prepare_records_update_dict(self, node):
update_dict = defaultdict(list)
for rdataset in node:
for rdata in rdataset:
option_name = (record_name_format % rdatatype.to_text(
rdata.rdtype).lower())
update_dict[option_name].append(unicode(rdata.to_text()))
return update_dict
def __update_dns_records(
self, record_name, nodes, set_cname_template=True
):
update_dict = self.__prepare_records_update_dict(nodes)
cname_template = {
'addattr': ['objectclass=idnsTemplateObject'],
'setattr': [
r'idnsTemplateAttribute;cnamerecord=%s'
r'.\{substitutionvariable_ipalocation\}._locations' %
record_name.relativize(self.domain_abs)
]
}
try:
if set_cname_template:
# only srv records should have configured cname templates
update_dict.update(cname_template)
self.api_instance.Command.dnsrecord_mod(
self.domain_abs, record_name,
**update_dict
)
except errors.NotFound:
# because internal API magic, addattr and setattr doesn't work with
# dnsrecord-add well, use dnsrecord-mod instead later
update_dict.pop('addattr', None)
update_dict.pop('setattr', None)
self.api_instance.Command.dnsrecord_add(
self.domain_abs, record_name, **update_dict)
if set_cname_template:
try:
self.api_instance.Command.dnsrecord_mod(
self.domain_abs,
record_name, **cname_template)
except errors.EmptyModlist:
pass
except errors.EmptyModlist:
pass
def get_base_records(
self, servers=None, roles=None, include_master_role=True,
include_kerberos_realm=True
):
"""
Generate IPA service records for specific servers and roles
:param servers: list of server which will be used in records,
if None all IPA servers will be used
:param roles: roles for which DNS records will be generated,
if None all roles will be used
:param include_master_role: generate records required by IPA master
role
:return: dns.zone.Zone object that contains base DNS records
"""
zone_obj = zone.Zone(self.domain_abs, relativize=False)
if servers is None:
servers = list(self.servers_data)
for server in servers:
self._add_base_dns_records_for_server(zone_obj, server,
roles=roles, include_master_role=include_master_role,
include_kerberos_realm=include_kerberos_realm
)
return zone_obj
def get_locations_records(
self, servers=None, roles=None, include_master_role=True,
include_kerberos_realm=True
):
"""
Generate IPA location records for specific servers and roles.
:param servers: list of server which will be used in records,
if None all IPA servers will be used
:param roles: roles for which DNS records will be generated,
if None all roles will be used
:param include_master_role: generate records required by IPA master
role
:return: dns.zone.Zone object that contains location DNS records
"""
zone_obj = zone.Zone(self.domain_abs, relativize=False)
if servers is None:
servers = list(self.servers_data)
locations_result = self.api_instance.Command.location_find()['result']
locations = [l['idnsname'][0] for l in locations_result]
for server in servers:
self._get_location_dns_records_for_server(
zone_obj, server,
locations, roles=roles,
include_master_role=include_master_role,
include_kerberos_realm=include_kerberos_realm)
return zone_obj
def update_base_records(self):
"""
Update base DNS records for IPA services
:return: [(record_name, node), ...], [(record_name, node, error), ...]
where the first list contains successfully updated records, and the
second list contains failed updates with particular exceptions
"""
fail = []
success = []
names_requiring_cname_templates = set(
rec[0].derelativize(self.domain_abs) for rec in (
IPA_DEFAULT_MASTER_SRV_REC
+ IPA_DEFAULT_MASTER_URI_REC
+ IPA_DEFAULT_KRB_TXT_REC
+ IPA_DEFAULT_ADTRUST_SRV_REC
+ IPA_DEFAULT_NTP_SRV_REC
)
)
# Remove the ipa-ca record(s). They will be reconstructed in
# get_base_records().
r_name = DNSName(IPA_CA_RECORD) + self.domain_abs
try:
self.api_instance.Command.dnsrecord_del(
self.domain_abs, r_name, del_all=True)
except errors.NotFound:
pass
base_zone = self.get_base_records()
for record_name, node in base_zone.items():
set_cname_template = record_name in names_requiring_cname_templates
try:
self.__update_dns_records(
record_name, node, set_cname_template)
except errors.PublicError as e:
fail.append((record_name, node, e))
else:
success.append((record_name, node))
return success, fail
def update_locations_records(self):
"""
Update locations DNS records for IPA services
:return: [(record_name, node), ...], [(record_name, node, error), ...]
where the first list contains successfully updated records, and the
second list contains failed updates with particular exceptions
"""
fail = []
success = []
location_zone = self.get_locations_records()
for record_name, nodes in location_zone.items():
try:
self.__update_dns_records(
record_name, nodes,
set_cname_template=False)
except errors.PublicError as e:
fail.append((record_name, nodes, e))
else:
success.append((record_name, nodes))
return success, fail
def update_dns_records(self):
"""
Update all IPA DNS records
:return: (sucessfully_updated_base_records, failed_base_records,
sucessfully_updated_locations_records, failed_locations_records)
For format see update_base_records or update_locations_method
:raise IPADomainIsNotManagedByIPAError: if IPA domain is not managed by
IPA DNS
"""
try:
self.api_instance.Command.dnszone_show(self.domain_abs)
except errors.NotFound:
raise IPADomainIsNotManagedByIPAError()
return (
self.update_base_records(),
self.update_locations_records()
)
def remove_location_records(self, location):
"""
Remove all location records
:param location: DNSName object
:return: list of successfuly removed record names, list of record
names that cannot be removed and returned exception in tuples
[rname1, ...], [(rname2, exc), ...]
"""
success = []
failed = []
location = DNSName(location)
loc_records = []
for records in (
IPA_DEFAULT_MASTER_SRV_REC,
IPA_DEFAULT_ADTRUST_SRV_REC,
IPA_DEFAULT_NTP_SRV_REC
):
for name, _port in records:
loc_records.append(
name + self.__get_location_suffix(location))
for rname in loc_records:
try:
self.api_instance.Command.dnsrecord_del(
self.domain_abs, rname, del_all=True)
except errors.NotFound:
pass
except errors.PublicError as e:
failed.append((rname, e))
else:
success.append(rname)
return success, failed
@classmethod
def records_list_from_node(cls, name, node):
records = []
for rdataset in node:
for rd in rdataset:
records.append(
'{name} {ttl} {rdclass} {rdtype} {rdata}'.format(
name=name.ToASCII(),
ttl=rdataset.ttl,
rdclass=rdataclass.to_text(rd.rdclass),
rdtype=rdatatype.to_text(rd.rdtype),
rdata=rd.to_text()
)
)
return records
@classmethod
def records_list_from_zone(cls, zone_obj, sort=True):
records = []
for name, node in zone_obj.items():
records.extend(IPASystemRecords.records_list_from_node(name, node))
if sort:
records.sort()
return records