1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
2
3# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
4#
5# Permission to use, copy, modify, and distribute this software and its
6# documentation for any purpose with or without fee is hereby granted,
7# provided that the above copyright notice and this permission notice
8# appear in all copies.
9#
10# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
16# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
18"""DNS Dynamic Update Support"""
19
20
21import dns.message
22import dns.name
23import dns.opcode
24import dns.rdata
25import dns.rdataclass
26import dns.rdataset
27import dns.tsig
28
29
30class UpdateSection(dns.enum.IntEnum):
31    """Update sections"""
32    ZONE = 0
33    PREREQ = 1
34    UPDATE = 2
35    ADDITIONAL = 3
36
37    @classmethod
38    def _maximum(cls):
39        return 3
40
41
42class UpdateMessage(dns.message.Message):
43
44    _section_enum = UpdateSection
45
46    def __init__(self, zone=None, rdclass=dns.rdataclass.IN, keyring=None,
47                 keyname=None, keyalgorithm=dns.tsig.default_algorithm,
48                 id=None):
49        """Initialize a new DNS Update object.
50
51        See the documentation of the Message class for a complete
52        description of the keyring dictionary.
53
54        *zone*, a ``dns.name.Name``, ``str``, or ``None``, the zone
55        which is being updated.  ``None`` should only be used by dnspython's
56        message constructors, as a zone is required for the convenience
57        methods like ``add()``, ``replace()``, etc.
58
59        *rdclass*, an ``int`` or ``str``, the class of the zone.
60
61        The *keyring*, *keyname*, and *keyalgorithm* parameters are passed to
62        ``use_tsig()``; see its documentation for details.
63        """
64        super().__init__(id=id)
65        self.flags |= dns.opcode.to_flags(dns.opcode.UPDATE)
66        if isinstance(zone, str):
67            zone = dns.name.from_text(zone)
68        self.origin = zone
69        rdclass = dns.rdataclass.RdataClass.make(rdclass)
70        self.zone_rdclass = rdclass
71        if self.origin:
72            self.find_rrset(self.zone, self.origin, rdclass, dns.rdatatype.SOA,
73                            create=True, force_unique=True)
74        if keyring is not None:
75            self.use_tsig(keyring, keyname, algorithm=keyalgorithm)
76
77    @property
78    def zone(self):
79        """The zone section."""
80        return self.sections[0]
81
82    @zone.setter
83    def zone(self, v):
84        self.sections[0] = v
85
86    @property
87    def prerequisite(self):
88        """The prerequisite section."""
89        return self.sections[1]
90
91    @prerequisite.setter
92    def prerequisite(self, v):
93        self.sections[1] = v
94
95    @property
96    def update(self):
97        """The update section."""
98        return self.sections[2]
99
100    @update.setter
101    def update(self, v):
102        self.sections[2] = v
103
104    def _add_rr(self, name, ttl, rd, deleting=None, section=None):
105        """Add a single RR to the update section."""
106
107        if section is None:
108            section = self.update
109        covers = rd.covers()
110        rrset = self.find_rrset(section, name, self.zone_rdclass, rd.rdtype,
111                                covers, deleting, True, True)
112        rrset.add(rd, ttl)
113
114    def _add(self, replace, section, name, *args):
115        """Add records.
116
117        *replace* is the replacement mode.  If ``False``,
118        RRs are added to an existing RRset; if ``True``, the RRset
119        is replaced with the specified contents.  The second
120        argument is the section to add to.  The third argument
121        is always a name.  The other arguments can be:
122
123                - rdataset...
124
125                - ttl, rdata...
126
127                - ttl, rdtype, string...
128        """
129
130        if isinstance(name, str):
131            name = dns.name.from_text(name, None)
132        if isinstance(args[0], dns.rdataset.Rdataset):
133            for rds in args:
134                if replace:
135                    self.delete(name, rds.rdtype)
136                for rd in rds:
137                    self._add_rr(name, rds.ttl, rd, section=section)
138        else:
139            args = list(args)
140            ttl = int(args.pop(0))
141            if isinstance(args[0], dns.rdata.Rdata):
142                if replace:
143                    self.delete(name, args[0].rdtype)
144                for rd in args:
145                    self._add_rr(name, ttl, rd, section=section)
146            else:
147                rdtype = dns.rdatatype.RdataType.make(args.pop(0))
148                if replace:
149                    self.delete(name, rdtype)
150                for s in args:
151                    rd = dns.rdata.from_text(self.zone_rdclass, rdtype, s,
152                                             self.origin)
153                    self._add_rr(name, ttl, rd, section=section)
154
155    def add(self, name, *args):
156        """Add records.
157
158        The first argument is always a name.  The other
159        arguments can be:
160
161                - rdataset...
162
163                - ttl, rdata...
164
165                - ttl, rdtype, string...
166        """
167
168        self._add(False, self.update, name, *args)
169
170    def delete(self, name, *args):
171        """Delete records.
172
173        The first argument is always a name.  The other
174        arguments can be:
175
176                - *empty*
177
178                - rdataset...
179
180                - rdata...
181
182                - rdtype, [string...]
183        """
184
185        if isinstance(name, str):
186            name = dns.name.from_text(name, None)
187        if len(args) == 0:
188            self.find_rrset(self.update, name, dns.rdataclass.ANY,
189                            dns.rdatatype.ANY, dns.rdatatype.NONE,
190                            dns.rdatatype.ANY, True, True)
191        elif isinstance(args[0], dns.rdataset.Rdataset):
192            for rds in args:
193                for rd in rds:
194                    self._add_rr(name, 0, rd, dns.rdataclass.NONE)
195        else:
196            args = list(args)
197            if isinstance(args[0], dns.rdata.Rdata):
198                for rd in args:
199                    self._add_rr(name, 0, rd, dns.rdataclass.NONE)
200            else:
201                rdtype = dns.rdatatype.RdataType.make(args.pop(0))
202                if len(args) == 0:
203                    self.find_rrset(self.update, name,
204                                    self.zone_rdclass, rdtype,
205                                    dns.rdatatype.NONE,
206                                    dns.rdataclass.ANY,
207                                    True, True)
208                else:
209                    for s in args:
210                        rd = dns.rdata.from_text(self.zone_rdclass, rdtype, s,
211                                                 self.origin)
212                        self._add_rr(name, 0, rd, dns.rdataclass.NONE)
213
214    def replace(self, name, *args):
215        """Replace records.
216
217        The first argument is always a name.  The other
218        arguments can be:
219
220                - rdataset...
221
222                - ttl, rdata...
223
224                - ttl, rdtype, string...
225
226        Note that if you want to replace the entire node, you should do
227        a delete of the name followed by one or more calls to add.
228        """
229
230        self._add(True, self.update, name, *args)
231
232    def present(self, name, *args):
233        """Require that an owner name (and optionally an rdata type,
234        or specific rdataset) exists as a prerequisite to the
235        execution of the update.
236
237        The first argument is always a name.
238        The other arguments can be:
239
240                - rdataset...
241
242                - rdata...
243
244                - rdtype, string...
245        """
246
247        if isinstance(name, str):
248            name = dns.name.from_text(name, None)
249        if len(args) == 0:
250            self.find_rrset(self.prerequisite, name,
251                            dns.rdataclass.ANY, dns.rdatatype.ANY,
252                            dns.rdatatype.NONE, None,
253                            True, True)
254        elif isinstance(args[0], dns.rdataset.Rdataset) or \
255            isinstance(args[0], dns.rdata.Rdata) or \
256                len(args) > 1:
257            if not isinstance(args[0], dns.rdataset.Rdataset):
258                # Add a 0 TTL
259                args = list(args)
260                args.insert(0, 0)
261            self._add(False, self.prerequisite, name, *args)
262        else:
263            rdtype = dns.rdatatype.RdataType.make(args[0])
264            self.find_rrset(self.prerequisite, name,
265                            dns.rdataclass.ANY, rdtype,
266                            dns.rdatatype.NONE, None,
267                            True, True)
268
269    def absent(self, name, rdtype=None):
270        """Require that an owner name (and optionally an rdata type) does
271        not exist as a prerequisite to the execution of the update."""
272
273        if isinstance(name, str):
274            name = dns.name.from_text(name, None)
275        if rdtype is None:
276            self.find_rrset(self.prerequisite, name,
277                            dns.rdataclass.NONE, dns.rdatatype.ANY,
278                            dns.rdatatype.NONE, None,
279                            True, True)
280        else:
281            rdtype = dns.rdatatype.RdataType.make(rdtype)
282            self.find_rrset(self.prerequisite, name,
283                            dns.rdataclass.NONE, rdtype,
284                            dns.rdatatype.NONE, None,
285                            True, True)
286
287    def _get_one_rr_per_rrset(self, value):
288        # Updates are always one_rr_per_rrset
289        return True
290
291    def _parse_rr_header(self, section, name, rdclass, rdtype):
292        deleting = None
293        empty = False
294        if section == UpdateSection.ZONE:
295            if dns.rdataclass.is_metaclass(rdclass) or \
296               rdtype != dns.rdatatype.SOA or \
297               self.zone:
298                raise dns.exception.FormError
299        else:
300            if not self.zone:
301                raise dns.exception.FormError
302            if rdclass in (dns.rdataclass.ANY, dns.rdataclass.NONE):
303                deleting = rdclass
304                rdclass = self.zone[0].rdclass
305                empty = (deleting == dns.rdataclass.ANY or
306                         section == UpdateSection.PREREQ)
307        return (rdclass, rdtype, deleting, empty)
308
309# backwards compatibility
310Update = UpdateMessage
311
312### BEGIN generated UpdateSection constants
313
314ZONE = UpdateSection.ZONE
315PREREQ = UpdateSection.PREREQ
316UPDATE = UpdateSection.UPDATE
317ADDITIONAL = UpdateSection.ADDITIONAL
318
319### END generated UpdateSection constants
320