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
28from ._compat import string_types
29
30
31class Update(dns.message.Message):
32
33    def __init__(self, zone, rdclass=dns.rdataclass.IN, keyring=None,
34                 keyname=None, keyalgorithm=dns.tsig.default_algorithm):
35        """Initialize a new DNS Update object.
36
37        See the documentation of the Message class for a complete
38        description of the keyring dictionary.
39
40        *zone*, a ``dns.name.Name`` or ``text``, the zone which is being
41        updated.
42
43        *rdclass*, an ``int`` or ``text``, the class of the zone.
44
45        *keyring*, a ``dict``, the TSIG keyring to use.  If a
46        *keyring* is specified but a *keyname* is not, then the key
47        used will be the first key in the *keyring*.  Note that the
48        order of keys in a dictionary is not defined, so applications
49        should supply a keyname when a keyring is used, unless they
50        know the keyring contains only one key.
51
52        *keyname*, a ``dns.name.Name`` or ``None``, the name of the TSIG key
53        to use; defaults to ``None``. The key must be defined in the keyring.
54
55        *keyalgorithm*, a ``dns.name.Name``, the TSIG algorithm to use.
56        """
57        super(Update, self).__init__()
58        self.flags |= dns.opcode.to_flags(dns.opcode.UPDATE)
59        if isinstance(zone, string_types):
60            zone = dns.name.from_text(zone)
61        self.origin = zone
62        if isinstance(rdclass, string_types):
63            rdclass = dns.rdataclass.from_text(rdclass)
64        self.zone_rdclass = rdclass
65        self.find_rrset(self.question, self.origin, rdclass, dns.rdatatype.SOA,
66                        create=True, force_unique=True)
67        if keyring is not None:
68            self.use_tsig(keyring, keyname, algorithm=keyalgorithm)
69
70    def _add_rr(self, name, ttl, rd, deleting=None, section=None):
71        """Add a single RR to the update section."""
72
73        if section is None:
74            section = self.authority
75        covers = rd.covers()
76        rrset = self.find_rrset(section, name, self.zone_rdclass, rd.rdtype,
77                                covers, deleting, True, True)
78        rrset.add(rd, ttl)
79
80    def _add(self, replace, section, name, *args):
81        """Add records.
82
83        *replace* is the replacement mode.  If ``False``,
84        RRs are added to an existing RRset; if ``True``, the RRset
85        is replaced with the specified contents.  The second
86        argument is the section to add to.  The third argument
87        is always a name.  The other arguments can be:
88
89                - rdataset...
90
91                - ttl, rdata...
92
93                - ttl, rdtype, string...
94        """
95
96        if isinstance(name, string_types):
97            name = dns.name.from_text(name, None)
98        if isinstance(args[0], dns.rdataset.Rdataset):
99            for rds in args:
100                if replace:
101                    self.delete(name, rds.rdtype)
102                for rd in rds:
103                    self._add_rr(name, rds.ttl, rd, section=section)
104        else:
105            args = list(args)
106            ttl = int(args.pop(0))
107            if isinstance(args[0], dns.rdata.Rdata):
108                if replace:
109                    self.delete(name, args[0].rdtype)
110                for rd in args:
111                    self._add_rr(name, ttl, rd, section=section)
112            else:
113                rdtype = args.pop(0)
114                if isinstance(rdtype, string_types):
115                    rdtype = dns.rdatatype.from_text(rdtype)
116                if replace:
117                    self.delete(name, rdtype)
118                for s in args:
119                    rd = dns.rdata.from_text(self.zone_rdclass, rdtype, s,
120                                             self.origin)
121                    self._add_rr(name, ttl, rd, section=section)
122
123    def add(self, name, *args):
124        """Add records.
125
126        The first argument is always a name.  The other
127        arguments can be:
128
129                - rdataset...
130
131                - ttl, rdata...
132
133                - ttl, rdtype, string...
134        """
135
136        self._add(False, self.authority, name, *args)
137
138    def delete(self, name, *args):
139        """Delete records.
140
141        The first argument is always a name.  The other
142        arguments can be:
143
144                - *empty*
145
146                - rdataset...
147
148                - rdata...
149
150                - rdtype, [string...]
151        """
152
153        if isinstance(name, string_types):
154            name = dns.name.from_text(name, None)
155        if len(args) == 0:
156            self.find_rrset(self.authority, name, dns.rdataclass.ANY,
157                            dns.rdatatype.ANY, dns.rdatatype.NONE,
158                            dns.rdatatype.ANY, True, True)
159        elif isinstance(args[0], dns.rdataset.Rdataset):
160            for rds in args:
161                for rd in rds:
162                    self._add_rr(name, 0, rd, dns.rdataclass.NONE)
163        else:
164            args = list(args)
165            if isinstance(args[0], dns.rdata.Rdata):
166                for rd in args:
167                    self._add_rr(name, 0, rd, dns.rdataclass.NONE)
168            else:
169                rdtype = args.pop(0)
170                if isinstance(rdtype, string_types):
171                    rdtype = dns.rdatatype.from_text(rdtype)
172                if len(args) == 0:
173                    self.find_rrset(self.authority, name,
174                                    self.zone_rdclass, rdtype,
175                                    dns.rdatatype.NONE,
176                                    dns.rdataclass.ANY,
177                                    True, True)
178                else:
179                    for s in args:
180                        rd = dns.rdata.from_text(self.zone_rdclass, rdtype, s,
181                                                 self.origin)
182                        self._add_rr(name, 0, rd, dns.rdataclass.NONE)
183
184    def replace(self, name, *args):
185        """Replace records.
186
187        The first argument is always a name.  The other
188        arguments can be:
189
190                - rdataset...
191
192                - ttl, rdata...
193
194                - ttl, rdtype, string...
195
196        Note that if you want to replace the entire node, you should do
197        a delete of the name followed by one or more calls to add.
198        """
199
200        self._add(True, self.authority, name, *args)
201
202    def present(self, name, *args):
203        """Require that an owner name (and optionally an rdata type,
204        or specific rdataset) exists as a prerequisite to the
205        execution of the update.
206
207        The first argument is always a name.
208        The other arguments can be:
209
210                - rdataset...
211
212                - rdata...
213
214                - rdtype, string...
215        """
216
217        if isinstance(name, string_types):
218            name = dns.name.from_text(name, None)
219        if len(args) == 0:
220            self.find_rrset(self.answer, name,
221                            dns.rdataclass.ANY, dns.rdatatype.ANY,
222                            dns.rdatatype.NONE, None,
223                            True, True)
224        elif isinstance(args[0], dns.rdataset.Rdataset) or \
225            isinstance(args[0], dns.rdata.Rdata) or \
226                len(args) > 1:
227            if not isinstance(args[0], dns.rdataset.Rdataset):
228                # Add a 0 TTL
229                args = list(args)
230                args.insert(0, 0)
231            self._add(False, self.answer, name, *args)
232        else:
233            rdtype = args[0]
234            if isinstance(rdtype, string_types):
235                rdtype = dns.rdatatype.from_text(rdtype)
236            self.find_rrset(self.answer, name,
237                            dns.rdataclass.ANY, rdtype,
238                            dns.rdatatype.NONE, None,
239                            True, True)
240
241    def absent(self, name, rdtype=None):
242        """Require that an owner name (and optionally an rdata type) does
243        not exist as a prerequisite to the execution of the update."""
244
245        if isinstance(name, string_types):
246            name = dns.name.from_text(name, None)
247        if rdtype is None:
248            self.find_rrset(self.answer, name,
249                            dns.rdataclass.NONE, dns.rdatatype.ANY,
250                            dns.rdatatype.NONE, None,
251                            True, True)
252        else:
253            if isinstance(rdtype, string_types):
254                rdtype = dns.rdatatype.from_text(rdtype)
255            self.find_rrset(self.answer, name,
256                            dns.rdataclass.NONE, rdtype,
257                            dns.rdatatype.NONE, None,
258                            True, True)
259
260    def to_wire(self, origin=None, max_size=65535):
261        """Return a string containing the update in DNS compressed wire
262        format.
263
264        *origin*, a ``dns.name.Name`` or ``None``, the origin to be
265        appended to any relative names.  If *origin* is ``None``, then
266        the origin of the ``dns.update.Update`` message object is used
267        (i.e. the *zone* parameter passed when the Update object was
268        created).
269
270        *max_size*, an ``int``, the maximum size of the wire format
271        output; default is 0, which means "the message's request
272        payload, if nonzero, or 65535".
273
274        Returns a ``binary``.
275        """
276
277        if origin is None:
278            origin = self.origin
279        return super(Update, self).to_wire(origin, max_size)
280