summaryrefslogtreecommitdiffstats
path: root/ipapython/dnssec/odsmgr.py
blob: ed17351cad4db28caed2623aa4bec145d5bc4414 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#!/usr/bin/python
#
# Copyright (C) 2014  FreeIPA Contributors see COPYING for license
#

import logging
from lxml import etree
import dns.name
import subprocess

from ipapython import ipa_log_manager, ipautil

# 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(object):
    def __init__(self):
        self.names = set()  # dns.name
        self.uuids = set()  # UUID strings
        self.mapping = dict()      # {UUID: dns.name}
        self.log = ipa_log_manager.log_mgr.get_logger(self)

    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__()
        xml = etree.fromstring(zonelist_text)
        self._parse_zonelist(xml)

    def _parse_zonelist(self, xml):
        """iterate over Zone elements with attribute 'name' and
        add IPA zones to self.zones"""
        for zone_xml in xml.xpath('/ZoneList/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')
        in_adapters = zone_xml.xpath(
            'Adapters/Input/Adapter[@type="File" '
            'and starts-with(text(), "%s")]' % ENTRYUUID_PREFIX)
        assert len(in_adapters) == 1, 'only IPA zones are supported: %s' \
            % etree.tostring(zone_xml)

        path = in_adapters[0].text
        # strip prefix from path
        zid = path[ENTRYUUID_PREFIX_LEN:]
        return (name, zid)


class LDAPZoneListReader(ZoneListReader):
    def __init__(self):
        super(LDAPZoneListReader, self).__init__()

    def process_ipa_zone(self, op, uuid, zone_ldap):
        assert (op == 'add' or op == '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(object):
    """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.log = ipa_log_manager.log_mgr.get_logger(self)
        self.zl_ldap = LDAPZoneListReader()

    def ksmutil(self, params):
        """Call ods-ksmutil with given parameters and return stdout.

        Raises CalledProcessError if returncode != 0.
        """
        cmd = ['ods-ksmutil'] + params
        return ipautil.run(cmd)[0]

    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)
        self.log.info(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)
        self.log.info(output)
        self.notify_enforcer()

    def notify_enforcer(self):
        cmd = ['notify']
        output = self.ksmutil(cmd)
        self.log.info(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 == 'add' or op == 'del'
        self.zl_ldap.process_ipa_zone(op, uuid, attrs)
        self.log.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()
        self.log.debug("ODS zones: %s", zl_ods.mapping)
        removed = self.diff_zl(zl_ods, self.zl_ldap)
        self.log.info("Zones removed from LDAP: %s", removed)
        added = self.diff_zl(self.zl_ldap, zl_ods)
        self.log.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()
    ipa_log_manager.root_logger.info('ODS zones: %s', reader.mapping)