mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-25 08:21:05 -06:00
edaea8865f
OpenDNSSEC 1.4 and 2.x use different commands to initialize kasp.db and manage zones. ipaplatform.tasks abstracts the commands. Note: I added the logic to the base task instead of having different implementations for Red Hat and Debian platforms. Eventually Fedora is going to move to OpenDNSSEC 2.x, too. The design will make it easier to support OpenDNSSEC 2.x on Fedora. Signed-off-by: Christian Heimes <cheimes@redhat.com> Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com> Reviewed-By: Rob Crittenden <rcritten@redhat.com>
212 lines
7.2 KiB
Python
212 lines
7.2 KiB
Python
#
|
|
# Copyright (C) 2014 FreeIPA Contributors see COPYING for license
|
|
#
|
|
|
|
import logging
|
|
|
|
import dns.name
|
|
try:
|
|
from xml.etree import cElementTree as etree
|
|
except ImportError:
|
|
from xml.etree import ElementTree as etree
|
|
|
|
from ipapython import ipa_log_manager, ipautil
|
|
from ipaplatform.tasks import tasks
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# hack: zone object UUID is stored as path to imaginary zone file
|
|
ENTRYUUID_PREFIX = "/var/lib/ipa/dns/zone/entryUUID/"
|
|
ENTRYUUID_PREFIX_LEN = len(ENTRYUUID_PREFIX)
|
|
|
|
|
|
class ZoneListReader:
|
|
def __init__(self):
|
|
self.names = set() # dns.name
|
|
self.uuids = set() # UUID strings
|
|
self.mapping = dict() # {UUID: dns.name}
|
|
|
|
def _add_zone(self, name, zid):
|
|
"""Add zone & UUID to internal structures.
|
|
|
|
Zone with given name and UUID must not exist."""
|
|
# detect duplicate zone names
|
|
name = dns.name.from_text(name)
|
|
assert name not in self.names, \
|
|
'duplicate name (%s, %s) vs. %s' % (name, zid, self.mapping)
|
|
# duplicate non-None zid is not allowed
|
|
assert not zid or zid not in self.uuids, \
|
|
'duplicate UUID (%s, %s) vs. %s' % (name, zid, self.mapping)
|
|
|
|
self.names.add(name)
|
|
self.uuids.add(zid)
|
|
self.mapping[zid] = name
|
|
|
|
def _del_zone(self, name, zid):
|
|
"""Remove zone & UUID from internal structures.
|
|
|
|
Zone with given name and UUID must exist.
|
|
"""
|
|
name = dns.name.from_text(name)
|
|
assert zid is not None
|
|
assert name in self.names, \
|
|
'name (%s, %s) does not exist in %s' % (name, zid, self.mapping)
|
|
assert zid in self.uuids, \
|
|
'UUID (%s, %s) does not exist in %s' % (name, zid, self.mapping)
|
|
assert zid in self.mapping and name == self.mapping[zid], \
|
|
'pair {%s: %s} does not exist in %s' % (zid, name, self.mapping)
|
|
|
|
self.names.remove(name)
|
|
self.uuids.remove(zid)
|
|
del self.mapping[zid]
|
|
|
|
|
|
class ODSZoneListReader(ZoneListReader):
|
|
"""One-shot parser for ODS zonelist.xml."""
|
|
def __init__(self, zonelist_text):
|
|
super(ODSZoneListReader, self).__init__()
|
|
root = etree.fromstring(zonelist_text)
|
|
self._parse_zonelist(root)
|
|
|
|
def _parse_zonelist(self, root):
|
|
"""iterate over Zone elements with attribute 'name' and
|
|
add IPA zones to self.zones"""
|
|
if not root.tag == 'ZoneList':
|
|
raise ValueError(root.tag)
|
|
for zone_xml in root.findall('./Zone[@name]'):
|
|
name, zid = self._parse_ipa_zone(zone_xml)
|
|
self._add_zone(name, zid)
|
|
|
|
def _parse_ipa_zone(self, zone_xml):
|
|
"""Extract zone name, input adapter and detect IPA zones.
|
|
|
|
IPA zones have contains Adapters/Input/Adapter element with
|
|
attribute type = "File" and with value prefixed with ENTRYUUID_PREFIX.
|
|
|
|
Returns:
|
|
tuple (zone name, ID)
|
|
"""
|
|
name = zone_xml.get('name')
|
|
zids = []
|
|
for in_adapter in zone_xml.findall(
|
|
'./Adapters/Input/Adapter[@type="File"]'):
|
|
path = in_adapter.text
|
|
if path.startswith(ENTRYUUID_PREFIX):
|
|
# strip prefix from path
|
|
zids.append(path[ENTRYUUID_PREFIX_LEN:])
|
|
|
|
if len(zids) != 1:
|
|
raise ValueError('only IPA zones are supported: {}'.format(
|
|
etree.tostring(zone_xml)))
|
|
|
|
return name, zids[0]
|
|
|
|
|
|
class LDAPZoneListReader(ZoneListReader):
|
|
def __init__(self):
|
|
super(LDAPZoneListReader, self).__init__()
|
|
|
|
def process_ipa_zone(self, op, uuid, zone_ldap):
|
|
assert (op in ['add', 'del']), 'unsupported op %s' % op
|
|
assert uuid is not None
|
|
assert 'idnsname' in zone_ldap, \
|
|
'LDAP zone UUID %s without idnsName' % uuid
|
|
assert len(zone_ldap['idnsname']) == 1, \
|
|
'LDAP zone UUID %s with len(idnsname) != 1' % uuid
|
|
|
|
if op == 'add':
|
|
self._add_zone(zone_ldap['idnsname'][0], uuid)
|
|
elif op == 'del':
|
|
self._del_zone(zone_ldap['idnsname'][0], uuid)
|
|
|
|
|
|
class ODSMgr:
|
|
"""OpenDNSSEC zone manager. It does LDAP->ODS synchronization.
|
|
|
|
Zones with idnsSecInlineSigning attribute = TRUE in LDAP are added
|
|
or deleted from ODS as necessary. ODS->LDAP key synchronization
|
|
has to be solved seperatelly.
|
|
"""
|
|
def __init__(self):
|
|
self.zl_ldap = LDAPZoneListReader()
|
|
|
|
def ksmutil(self, params):
|
|
"""Call ods-ksmutil / ods-enforcer with parameters and return stdout.
|
|
|
|
Raises CalledProcessError if returncode != 0.
|
|
"""
|
|
result = tasks.run_ods_manager(params, capture_output=True)
|
|
return result.output
|
|
|
|
def get_ods_zonelist(self):
|
|
stdout = self.ksmutil(['zonelist', 'export'])
|
|
reader = ODSZoneListReader(stdout)
|
|
return reader
|
|
|
|
def add_ods_zone(self, uuid, name):
|
|
zone_path = '%s%s' % (ENTRYUUID_PREFIX, uuid)
|
|
cmd = ['zone', 'add', '--zone', str(name), '--input', zone_path]
|
|
output = self.ksmutil(cmd)
|
|
logger.info('%s', output)
|
|
self.notify_enforcer()
|
|
|
|
def del_ods_zone(self, name):
|
|
# ods-ksmutil blows up if zone name has period at the end
|
|
name = name.relativize(dns.name.root)
|
|
# detect if name is root zone
|
|
if name == dns.name.empty:
|
|
name = dns.name.root
|
|
cmd = ['zone', 'delete', '--zone', str(name)]
|
|
output = self.ksmutil(cmd)
|
|
logger.info('%s', output)
|
|
self.notify_enforcer()
|
|
self.cleanup_signer(name)
|
|
|
|
def notify_enforcer(self):
|
|
cmd = ['notify']
|
|
output = self.ksmutil(cmd)
|
|
logger.info('%s', output)
|
|
|
|
def cleanup_signer(self, zone_name):
|
|
cmd = ['ods-signer', 'ldap-cleanup', str(zone_name)]
|
|
output = ipautil.run(cmd, capture_output=True)
|
|
logger.info('%s', output)
|
|
|
|
def ldap_event(self, op, uuid, attrs):
|
|
"""Record single LDAP event - zone addition or deletion.
|
|
|
|
Change is only recorded to memory.
|
|
self.sync() have to be called to synchronize change to ODS."""
|
|
assert op in ('add', 'del')
|
|
self.zl_ldap.process_ipa_zone(op, uuid, attrs)
|
|
logger.debug("LDAP zones: %s", self.zl_ldap.mapping)
|
|
|
|
def sync(self):
|
|
"""Synchronize list of zones in LDAP with ODS."""
|
|
zl_ods = self.get_ods_zonelist()
|
|
logger.debug("ODS zones: %s", zl_ods.mapping)
|
|
removed = self.diff_zl(zl_ods, self.zl_ldap)
|
|
logger.info("Zones removed from LDAP: %s", removed)
|
|
added = self.diff_zl(self.zl_ldap, zl_ods)
|
|
logger.info("Zones added to LDAP: %s", added)
|
|
for (uuid, name) in removed:
|
|
self.del_ods_zone(name)
|
|
for (uuid, name) in added:
|
|
self.add_ods_zone(uuid, name)
|
|
|
|
def diff_zl(self, s1, s2):
|
|
"""Compute zones present in s1 but not present in s2.
|
|
|
|
Returns: List of (uuid, name) tuples with zones present only in s1."""
|
|
s1_extra = s1.uuids - s2.uuids
|
|
removed = [(uuid, name) for (uuid, name) in s1.mapping.items()
|
|
if uuid in s1_extra]
|
|
return removed
|
|
|
|
|
|
if __name__ == '__main__':
|
|
ipa_log_manager.standard_logging_setup(debug=True)
|
|
ods = ODSMgr()
|
|
reader = ods.get_ods_zonelist()
|
|
logger.info('ODS zones: %s', reader.mapping)
|