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 Zones."""
19
20import contextlib
21import io
22import os
23
24import dns.exception
25import dns.name
26import dns.node
27import dns.rdataclass
28import dns.rdatatype
29import dns.rdata
30import dns.rdtypes.ANY.SOA
31import dns.rrset
32import dns.tokenizer
33import dns.transaction
34import dns.ttl
35import dns.grange
36import dns.zonefile
37
38
39class BadZone(dns.exception.DNSException):
40
41    """The DNS zone is malformed."""
42
43
44class NoSOA(BadZone):
45
46    """The DNS zone has no SOA RR at its origin."""
47
48
49class NoNS(BadZone):
50
51    """The DNS zone has no NS RRset at its origin."""
52
53
54class UnknownOrigin(BadZone):
55
56    """The DNS zone's origin is unknown."""
57
58
59class Zone(dns.transaction.TransactionManager):
60
61    """A DNS zone.
62
63    A ``Zone`` is a mapping from names to nodes.  The zone object may be
64    treated like a Python dictionary, e.g. ``zone[name]`` will retrieve
65    the node associated with that name.  The *name* may be a
66    ``dns.name.Name object``, or it may be a string.  In either case,
67    if the name is relative it is treated as relative to the origin of
68    the zone.
69    """
70
71    node_factory = dns.node.Node
72
73    __slots__ = ['rdclass', 'origin', 'nodes', 'relativize']
74
75    def __init__(self, origin, rdclass=dns.rdataclass.IN, relativize=True):
76        """Initialize a zone object.
77
78        *origin* is the origin of the zone.  It may be a ``dns.name.Name``,
79        a ``str``, or ``None``.  If ``None``, then the zone's origin will
80        be set by the first ``$ORIGIN`` line in a zone file.
81
82        *rdclass*, an ``int``, the zone's rdata class; the default is class IN.
83
84        *relativize*, a ``bool``, determine's whether domain names are
85        relativized to the zone's origin.  The default is ``True``.
86        """
87
88        if origin is not None:
89            if isinstance(origin, str):
90                origin = dns.name.from_text(origin)
91            elif not isinstance(origin, dns.name.Name):
92                raise ValueError("origin parameter must be convertible to a "
93                                 "DNS name")
94            if not origin.is_absolute():
95                raise ValueError("origin parameter must be an absolute name")
96        self.origin = origin
97        self.rdclass = rdclass
98        self.nodes = {}
99        self.relativize = relativize
100
101    def __eq__(self, other):
102        """Two zones are equal if they have the same origin, class, and
103        nodes.
104
105        Returns a ``bool``.
106        """
107
108        if not isinstance(other, Zone):
109            return False
110        if self.rdclass != other.rdclass or \
111           self.origin != other.origin or \
112           self.nodes != other.nodes:
113            return False
114        return True
115
116    def __ne__(self, other):
117        """Are two zones not equal?
118
119        Returns a ``bool``.
120        """
121
122        return not self.__eq__(other)
123
124    def _validate_name(self, name):
125        if isinstance(name, str):
126            name = dns.name.from_text(name, None)
127        elif not isinstance(name, dns.name.Name):
128            raise KeyError("name parameter must be convertible to a DNS name")
129        if name.is_absolute():
130            if not name.is_subdomain(self.origin):
131                raise KeyError(
132                    "name parameter must be a subdomain of the zone origin")
133            if self.relativize:
134                name = name.relativize(self.origin)
135        return name
136
137    def __getitem__(self, key):
138        key = self._validate_name(key)
139        return self.nodes[key]
140
141    def __setitem__(self, key, value):
142        key = self._validate_name(key)
143        self.nodes[key] = value
144
145    def __delitem__(self, key):
146        key = self._validate_name(key)
147        del self.nodes[key]
148
149    def __iter__(self):
150        return self.nodes.__iter__()
151
152    def keys(self):
153        return self.nodes.keys()  # pylint: disable=dict-keys-not-iterating
154
155    def values(self):
156        return self.nodes.values()  # pylint: disable=dict-values-not-iterating
157
158    def items(self):
159        return self.nodes.items()  # pylint: disable=dict-items-not-iterating
160
161    def get(self, key):
162        key = self._validate_name(key)
163        return self.nodes.get(key)
164
165    def __contains__(self, other):
166        return other in self.nodes
167
168    def find_node(self, name, create=False):
169        """Find a node in the zone, possibly creating it.
170
171        *name*: the name of the node to find.
172        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
173        name must be a subdomain of the zone's origin.  If ``zone.relativize``
174        is ``True``, then the name will be relativized.
175
176        *create*, a ``bool``.  If true, the node will be created if it does
177        not exist.
178
179        Raises ``KeyError`` if the name is not known and create was
180        not specified, or if the name was not a subdomain of the origin.
181
182        Returns a ``dns.node.Node``.
183        """
184
185        name = self._validate_name(name)
186        node = self.nodes.get(name)
187        if node is None:
188            if not create:
189                raise KeyError
190            node = self.node_factory()
191            self.nodes[name] = node
192        return node
193
194    def get_node(self, name, create=False):
195        """Get a node in the zone, possibly creating it.
196
197        This method is like ``find_node()``, except it returns None instead
198        of raising an exception if the node does not exist and creation
199        has not been requested.
200
201        *name*: the name of the node to find.
202        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
203        name must be a subdomain of the zone's origin.  If ``zone.relativize``
204        is ``True``, then the name will be relativized.
205
206        *create*, a ``bool``.  If true, the node will be created if it does
207        not exist.
208
209        Raises ``KeyError`` if the name is not known and create was
210        not specified, or if the name was not a subdomain of the origin.
211
212        Returns a ``dns.node.Node`` or ``None``.
213        """
214
215        try:
216            node = self.find_node(name, create)
217        except KeyError:
218            node = None
219        return node
220
221    def delete_node(self, name):
222        """Delete the specified node if it exists.
223
224        *name*: the name of the node to find.
225        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
226        name must be a subdomain of the zone's origin.  If ``zone.relativize``
227        is ``True``, then the name will be relativized.
228
229        It is not an error if the node does not exist.
230        """
231
232        name = self._validate_name(name)
233        if name in self.nodes:
234            del self.nodes[name]
235
236    def find_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,
237                      create=False):
238        """Look for an rdataset with the specified name and type in the zone,
239        and return an rdataset encapsulating it.
240
241        The rdataset returned is not a copy; changes to it will change
242        the zone.
243
244        KeyError is raised if the name or type are not found.
245
246        *name*: the name of the node to find.
247        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
248        name must be a subdomain of the zone's origin.  If ``zone.relativize``
249        is ``True``, then the name will be relativized.
250
251        *rdtype*, an ``int`` or ``str``, the rdata type desired.
252
253        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
254        Usually this value is ``dns.rdatatype.NONE``, but if the
255        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
256        then the covers value will be the rdata type the SIG/RRSIG
257        covers.  The library treats the SIG and RRSIG types as if they
258        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
259        This makes RRSIGs much easier to work with than if RRSIGs
260        covering different rdata types were aggregated into a single
261        RRSIG rdataset.
262
263        *create*, a ``bool``.  If true, the node will be created if it does
264        not exist.
265
266        Raises ``KeyError`` if the name is not known and create was
267        not specified, or if the name was not a subdomain of the origin.
268
269        Returns a ``dns.rdataset.Rdataset``.
270        """
271
272        name = self._validate_name(name)
273        rdtype = dns.rdatatype.RdataType.make(rdtype)
274        if covers is not None:
275            covers = dns.rdatatype.RdataType.make(covers)
276        node = self.find_node(name, create)
277        return node.find_rdataset(self.rdclass, rdtype, covers, create)
278
279    def get_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,
280                     create=False):
281        """Look for an rdataset with the specified name and type in the zone.
282
283        This method is like ``find_rdataset()``, except it returns None instead
284        of raising an exception if the rdataset does not exist and creation
285        has not been requested.
286
287        The rdataset returned is not a copy; changes to it will change
288        the zone.
289
290        *name*: the name of the node to find.
291        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
292        name must be a subdomain of the zone's origin.  If ``zone.relativize``
293        is ``True``, then the name will be relativized.
294
295        *rdtype*, an ``int`` or ``str``, the rdata type desired.
296
297        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
298        Usually this value is ``dns.rdatatype.NONE``, but if the
299        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
300        then the covers value will be the rdata type the SIG/RRSIG
301        covers.  The library treats the SIG and RRSIG types as if they
302        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
303        This makes RRSIGs much easier to work with than if RRSIGs
304        covering different rdata types were aggregated into a single
305        RRSIG rdataset.
306
307        *create*, a ``bool``.  If true, the node will be created if it does
308        not exist.
309
310        Raises ``KeyError`` if the name is not known and create was
311        not specified, or if the name was not a subdomain of the origin.
312
313        Returns a ``dns.rdataset.Rdataset`` or ``None``.
314        """
315
316        try:
317            rdataset = self.find_rdataset(name, rdtype, covers, create)
318        except KeyError:
319            rdataset = None
320        return rdataset
321
322    def delete_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE):
323        """Delete the rdataset matching *rdtype* and *covers*, if it
324        exists at the node specified by *name*.
325
326        It is not an error if the node does not exist, or if there is no
327        matching rdataset at the node.
328
329        If the node has no rdatasets after the deletion, it will itself
330        be deleted.
331
332        *name*: the name of the node to find.
333        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
334        name must be a subdomain of the zone's origin.  If ``zone.relativize``
335        is ``True``, then the name will be relativized.
336
337        *rdtype*, an ``int`` or ``str``, the rdata type desired.
338
339        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
340        Usually this value is ``dns.rdatatype.NONE``, but if the
341        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
342        then the covers value will be the rdata type the SIG/RRSIG
343        covers.  The library treats the SIG and RRSIG types as if they
344        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
345        This makes RRSIGs much easier to work with than if RRSIGs
346        covering different rdata types were aggregated into a single
347        RRSIG rdataset.
348        """
349
350        name = self._validate_name(name)
351        rdtype = dns.rdatatype.RdataType.make(rdtype)
352        if covers is not None:
353            covers = dns.rdatatype.RdataType.make(covers)
354        node = self.get_node(name)
355        if node is not None:
356            node.delete_rdataset(self.rdclass, rdtype, covers)
357            if len(node) == 0:
358                self.delete_node(name)
359
360    def replace_rdataset(self, name, replacement):
361        """Replace an rdataset at name.
362
363        It is not an error if there is no rdataset matching I{replacement}.
364
365        Ownership of the *replacement* object is transferred to the zone;
366        in other words, this method does not store a copy of *replacement*
367        at the node, it stores *replacement* itself.
368
369        If the node does not exist, it is created.
370
371        *name*: the name of the node to find.
372        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
373        name must be a subdomain of the zone's origin.  If ``zone.relativize``
374        is ``True``, then the name will be relativized.
375
376        *replacement*, a ``dns.rdataset.Rdataset``, the replacement rdataset.
377        """
378
379        if replacement.rdclass != self.rdclass:
380            raise ValueError('replacement.rdclass != zone.rdclass')
381        node = self.find_node(name, True)
382        node.replace_rdataset(replacement)
383
384    def find_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):
385        """Look for an rdataset with the specified name and type in the zone,
386        and return an RRset encapsulating it.
387
388        This method is less efficient than the similar
389        ``find_rdataset()`` because it creates an RRset instead of
390        returning the matching rdataset.  It may be more convenient
391        for some uses since it returns an object which binds the owner
392        name to the rdataset.
393
394        This method may not be used to create new nodes or rdatasets;
395        use ``find_rdataset`` instead.
396
397        *name*: the name of the node to find.
398        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
399        name must be a subdomain of the zone's origin.  If ``zone.relativize``
400        is ``True``, then the name will be relativized.
401
402        *rdtype*, an ``int`` or ``str``, the rdata type desired.
403
404        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
405        Usually this value is ``dns.rdatatype.NONE``, but if the
406        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
407        then the covers value will be the rdata type the SIG/RRSIG
408        covers.  The library treats the SIG and RRSIG types as if they
409        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
410        This makes RRSIGs much easier to work with than if RRSIGs
411        covering different rdata types were aggregated into a single
412        RRSIG rdataset.
413
414        *create*, a ``bool``.  If true, the node will be created if it does
415        not exist.
416
417        Raises ``KeyError`` if the name is not known and create was
418        not specified, or if the name was not a subdomain of the origin.
419
420        Returns a ``dns.rrset.RRset`` or ``None``.
421        """
422
423        name = self._validate_name(name)
424        rdtype = dns.rdatatype.RdataType.make(rdtype)
425        if covers is not None:
426            covers = dns.rdatatype.RdataType.make(covers)
427        rdataset = self.nodes[name].find_rdataset(self.rdclass, rdtype, covers)
428        rrset = dns.rrset.RRset(name, self.rdclass, rdtype, covers)
429        rrset.update(rdataset)
430        return rrset
431
432    def get_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):
433        """Look for an rdataset with the specified name and type in the zone,
434        and return an RRset encapsulating it.
435
436        This method is less efficient than the similar ``get_rdataset()``
437        because it creates an RRset instead of returning the matching
438        rdataset.  It may be more convenient for some uses since it
439        returns an object which binds the owner name to the rdataset.
440
441        This method may not be used to create new nodes or rdatasets;
442        use ``get_rdataset()`` instead.
443
444        *name*: the name of the node to find.
445        The value may be a ``dns.name.Name`` or a ``str``.  If absolute, the
446        name must be a subdomain of the zone's origin.  If ``zone.relativize``
447        is ``True``, then the name will be relativized.
448
449        *rdtype*, an ``int`` or ``str``, the rdata type desired.
450
451        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
452        Usually this value is ``dns.rdatatype.NONE``, but if the
453        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
454        then the covers value will be the rdata type the SIG/RRSIG
455        covers.  The library treats the SIG and RRSIG types as if they
456        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
457        This makes RRSIGs much easier to work with than if RRSIGs
458        covering different rdata types were aggregated into a single
459        RRSIG rdataset.
460
461        *create*, a ``bool``.  If true, the node will be created if it does
462        not exist.
463
464        Raises ``KeyError`` if the name is not known and create was
465        not specified, or if the name was not a subdomain of the origin.
466
467        Returns a ``dns.rrset.RRset`` or ``None``.
468        """
469
470        try:
471            rrset = self.find_rrset(name, rdtype, covers)
472        except KeyError:
473            rrset = None
474        return rrset
475
476    def iterate_rdatasets(self, rdtype=dns.rdatatype.ANY,
477                          covers=dns.rdatatype.NONE):
478        """Return a generator which yields (name, rdataset) tuples for
479        all rdatasets in the zone which have the specified *rdtype*
480        and *covers*.  If *rdtype* is ``dns.rdatatype.ANY``, the default,
481        then all rdatasets will be matched.
482
483        *rdtype*, an ``int`` or ``str``, the rdata type desired.
484
485        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
486        Usually this value is ``dns.rdatatype.NONE``, but if the
487        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
488        then the covers value will be the rdata type the SIG/RRSIG
489        covers.  The library treats the SIG and RRSIG types as if they
490        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
491        This makes RRSIGs much easier to work with than if RRSIGs
492        covering different rdata types were aggregated into a single
493        RRSIG rdataset.
494        """
495
496        rdtype = dns.rdatatype.RdataType.make(rdtype)
497        if covers is not None:
498            covers = dns.rdatatype.RdataType.make(covers)
499        for (name, node) in self.items():
500            for rds in node:
501                if rdtype == dns.rdatatype.ANY or \
502                   (rds.rdtype == rdtype and rds.covers == covers):
503                    yield (name, rds)
504
505    def iterate_rdatas(self, rdtype=dns.rdatatype.ANY,
506                       covers=dns.rdatatype.NONE):
507        """Return a generator which yields (name, ttl, rdata) tuples for
508        all rdatas in the zone which have the specified *rdtype*
509        and *covers*.  If *rdtype* is ``dns.rdatatype.ANY``, the default,
510        then all rdatas will be matched.
511
512        *rdtype*, an ``int`` or ``str``, the rdata type desired.
513
514        *covers*, an ``int`` or ``str`` or ``None``, the covered type.
515        Usually this value is ``dns.rdatatype.NONE``, but if the
516        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
517        then the covers value will be the rdata type the SIG/RRSIG
518        covers.  The library treats the SIG and RRSIG types as if they
519        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
520        This makes RRSIGs much easier to work with than if RRSIGs
521        covering different rdata types were aggregated into a single
522        RRSIG rdataset.
523        """
524
525        rdtype = dns.rdatatype.RdataType.make(rdtype)
526        if covers is not None:
527            covers = dns.rdatatype.RdataType.make(covers)
528        for (name, node) in self.items():
529            for rds in node:
530                if rdtype == dns.rdatatype.ANY or \
531                   (rds.rdtype == rdtype and rds.covers == covers):
532                    for rdata in rds:
533                        yield (name, rds.ttl, rdata)
534
535    def to_file(self, f, sorted=True, relativize=True, nl=None,
536                want_comments=False):
537        """Write a zone to a file.
538
539        *f*, a file or `str`.  If *f* is a string, it is treated
540        as the name of a file to open.
541
542        *sorted*, a ``bool``.  If True, the default, then the file
543        will be written with the names sorted in DNSSEC order from
544        least to greatest.  Otherwise the names will be written in
545        whatever order they happen to have in the zone's dictionary.
546
547        *relativize*, a ``bool``.  If True, the default, then domain
548        names in the output will be relativized to the zone's origin
549        if possible.
550
551        *nl*, a ``str`` or None.  The end of line string.  If not
552        ``None``, the output will use the platform's native
553        end-of-line marker (i.e. LF on POSIX, CRLF on Windows).
554
555        *want_comments*, a ``bool``.  If ``True``, emit end-of-line comments
556        as part of writing the file.  If ``False``, the default, do not
557        emit them.
558        """
559
560        with contextlib.ExitStack() as stack:
561            if isinstance(f, str):
562                f = stack.enter_context(open(f, 'wb'))
563
564            # must be in this way, f.encoding may contain None, or even
565            # attribute may not be there
566            file_enc = getattr(f, 'encoding', None)
567            if file_enc is None:
568                file_enc = 'utf-8'
569
570            if nl is None:
571                # binary mode, '\n' is not enough
572                nl_b = os.linesep.encode(file_enc)
573                nl = '\n'
574            elif isinstance(nl, str):
575                nl_b = nl.encode(file_enc)
576            else:
577                nl_b = nl
578                nl = nl.decode()
579
580            if sorted:
581                names = list(self.keys())
582                names.sort()
583            else:
584                names = self.keys()
585            for n in names:
586                l = self[n].to_text(n, origin=self.origin,
587                                    relativize=relativize,
588                                    want_comments=want_comments)
589                l_b = l.encode(file_enc)
590
591                try:
592                    f.write(l_b)
593                    f.write(nl_b)
594                except TypeError:  # textual mode
595                    f.write(l)
596                    f.write(nl)
597
598    def to_text(self, sorted=True, relativize=True, nl=None,
599                want_comments=False):
600        """Return a zone's text as though it were written to a file.
601
602        *sorted*, a ``bool``.  If True, the default, then the file
603        will be written with the names sorted in DNSSEC order from
604        least to greatest.  Otherwise the names will be written in
605        whatever order they happen to have in the zone's dictionary.
606
607        *relativize*, a ``bool``.  If True, the default, then domain
608        names in the output will be relativized to the zone's origin
609        if possible.
610
611        *nl*, a ``str`` or None.  The end of line string.  If not
612        ``None``, the output will use the platform's native
613        end-of-line marker (i.e. LF on POSIX, CRLF on Windows).
614
615        *want_comments*, a ``bool``.  If ``True``, emit end-of-line comments
616        as part of writing the file.  If ``False``, the default, do not
617        emit them.
618
619        Returns a ``str``.
620        """
621        temp_buffer = io.StringIO()
622        self.to_file(temp_buffer, sorted, relativize, nl, want_comments)
623        return_value = temp_buffer.getvalue()
624        temp_buffer.close()
625        return return_value
626
627    def check_origin(self):
628        """Do some simple checking of the zone's origin.
629
630        Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
631
632        Raises ``dns.zone.NoNS`` if there is no NS RRset.
633
634        Raises ``KeyError`` if there is no origin node.
635        """
636        if self.relativize:
637            name = dns.name.empty
638        else:
639            name = self.origin
640        if self.get_rdataset(name, dns.rdatatype.SOA) is None:
641            raise NoSOA
642        if self.get_rdataset(name, dns.rdatatype.NS) is None:
643            raise NoNS
644
645    # TransactionManager methods
646
647    def reader(self):
648        return Transaction(self, False, True)
649
650    def writer(self, replacement=False):
651        return Transaction(self, replacement, False)
652
653    def origin_information(self):
654        if self.relativize:
655            effective = dns.name.empty
656        else:
657            effective = self.origin
658        return (self.origin, self.relativize, effective)
659
660    def get_class(self):
661        return self.rdclass
662
663
664class Transaction(dns.transaction.Transaction):
665
666    _deleted_rdataset = dns.rdataset.Rdataset(dns.rdataclass.ANY,
667                                              dns.rdatatype.ANY)
668
669    def __init__(self, zone, replacement, read_only):
670        super().__init__(zone, replacement, read_only)
671        self.rdatasets = {}
672
673    @property
674    def zone(self):
675        return self.manager
676
677    def _get_rdataset(self, name, rdtype, covers):
678        rdataset = self.rdatasets.get((name, rdtype, covers))
679        if rdataset is self._deleted_rdataset:
680            return None
681        elif rdataset is None:
682            rdataset = self.zone.get_rdataset(name, rdtype, covers)
683        return rdataset
684
685    def _put_rdataset(self, name, rdataset):
686        assert not self.read_only
687        self.zone._validate_name(name)
688        self.rdatasets[(name, rdataset.rdtype, rdataset.covers)] = rdataset
689
690    def _delete_name(self, name):
691        assert not self.read_only
692        # First remove any changes involving the name
693        remove = []
694        for key in self.rdatasets:
695            if key[0] == name:
696                remove.append(key)
697        if len(remove) > 0:
698            for key in remove:
699                del self.rdatasets[key]
700        # Next add deletion records for any rdatasets matching the
701        # name in the zone
702        node = self.zone.get_node(name)
703        if node is not None:
704            for rdataset in node.rdatasets:
705                self.rdatasets[(name, rdataset.rdtype, rdataset.covers)] = \
706                    self._deleted_rdataset
707
708    def _delete_rdataset(self, name, rdtype, covers):
709        assert not self.read_only
710        try:
711            del self.rdatasets[(name, rdtype, covers)]
712        except KeyError:
713            pass
714        rdataset = self.zone.get_rdataset(name, rdtype, covers)
715        if rdataset is not None:
716            self.rdatasets[(name, rdataset.rdtype, rdataset.covers)] = \
717                self._deleted_rdataset
718
719    def _name_exists(self, name):
720        for key, rdataset in self.rdatasets.items():
721            if key[0] == name:
722                if rdataset != self._deleted_rdataset:
723                    return True
724                else:
725                    return None
726        self.zone._validate_name(name)
727        if self.zone.get_node(name):
728            return True
729        return False
730
731    def _changed(self):
732        if self.read_only:
733            return False
734        else:
735            return len(self.rdatasets) > 0
736
737    def _end_transaction(self, commit):
738        if commit and self._changed():
739            for (name, rdtype, covers), rdataset in \
740                self.rdatasets.items():
741                if rdataset is self._deleted_rdataset:
742                    self.zone.delete_rdataset(name, rdtype, covers)
743                else:
744                    self.zone.replace_rdataset(name, rdataset)
745
746    def _set_origin(self, origin):
747        if self.zone.origin is None:
748            self.zone.origin = origin
749
750    def _iterate_rdatasets(self):
751        # Expensive but simple!  Use a versioned zone for efficient txn
752        # iteration.
753        rdatasets = {}
754        for (name, rdataset) in self.zone.iterate_rdatasets():
755            rdatasets[(name, rdataset.rdtype, rdataset.covers)] = rdataset
756        rdatasets.update(self.rdatasets)
757        for (name, _, _), rdataset in rdatasets.items():
758            yield (name, rdataset)
759
760
761def from_text(text, origin=None, rdclass=dns.rdataclass.IN,
762              relativize=True, zone_factory=Zone, filename=None,
763              allow_include=False, check_origin=True, idna_codec=None):
764    """Build a zone object from a zone file format string.
765
766    *text*, a ``str``, the zone file format input.
767
768    *origin*, a ``dns.name.Name``, a ``str``, or ``None``.  The origin
769    of the zone; if not specified, the first ``$ORIGIN`` statement in the
770    zone file will determine the origin of the zone.
771
772    *rdclass*, an ``int``, the zone's rdata class; the default is class IN.
773
774    *relativize*, a ``bool``, determine's whether domain names are
775    relativized to the zone's origin.  The default is ``True``.
776
777    *zone_factory*, the zone factory to use or ``None``.  If ``None``, then
778    ``dns.zone.Zone`` will be used.  The value may be any class or callable
779    that returns a subclass of ``dns.zone.Zone``.
780
781    *filename*, a ``str`` or ``None``, the filename to emit when
782    describing where an error occurred; the default is ``'<string>'``.
783
784    *allow_include*, a ``bool``.  If ``True``, the default, then ``$INCLUDE``
785    directives are permitted.  If ``False``, then encoutering a ``$INCLUDE``
786    will raise a ``SyntaxError`` exception.
787
788    *check_origin*, a ``bool``.  If ``True``, the default, then sanity
789    checks of the origin node will be made by calling the zone's
790    ``check_origin()`` method.
791
792    *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
793    encoder/decoder.  If ``None``, the default IDNA 2003 encoder/decoder
794    is used.
795
796    Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
797
798    Raises ``dns.zone.NoNS`` if there is no NS RRset.
799
800    Raises ``KeyError`` if there is no origin node.
801
802    Returns a subclass of ``dns.zone.Zone``.
803    """
804
805    # 'text' can also be a file, but we don't publish that fact
806    # since it's an implementation detail.  The official file
807    # interface is from_file().
808
809    if filename is None:
810        filename = '<string>'
811    zone = zone_factory(origin, rdclass, relativize=relativize)
812    with zone.writer(True) as txn:
813        tok = dns.tokenizer.Tokenizer(text, filename, idna_codec=idna_codec)
814        reader = dns.zonefile.Reader(tok, rdclass, txn,
815                                     allow_include=allow_include)
816        try:
817            reader.read()
818        except dns.zonefile.UnknownOrigin:
819            # for backwards compatibility
820            raise dns.zone.UnknownOrigin
821    # Now that we're done reading, do some basic checking of the zone.
822    if check_origin:
823        zone.check_origin()
824    return zone
825
826
827def from_file(f, origin=None, rdclass=dns.rdataclass.IN,
828              relativize=True, zone_factory=Zone, filename=None,
829              allow_include=True, check_origin=True):
830    """Read a zone file and build a zone object.
831
832    *f*, a file or ``str``.  If *f* is a string, it is treated
833    as the name of a file to open.
834
835    *origin*, a ``dns.name.Name``, a ``str``, or ``None``.  The origin
836    of the zone; if not specified, the first ``$ORIGIN`` statement in the
837    zone file will determine the origin of the zone.
838
839    *rdclass*, an ``int``, the zone's rdata class; the default is class IN.
840
841    *relativize*, a ``bool``, determine's whether domain names are
842    relativized to the zone's origin.  The default is ``True``.
843
844    *zone_factory*, the zone factory to use or ``None``.  If ``None``, then
845    ``dns.zone.Zone`` will be used.  The value may be any class or callable
846    that returns a subclass of ``dns.zone.Zone``.
847
848    *filename*, a ``str`` or ``None``, the filename to emit when
849    describing where an error occurred; the default is ``'<string>'``.
850
851    *allow_include*, a ``bool``.  If ``True``, the default, then ``$INCLUDE``
852    directives are permitted.  If ``False``, then encoutering a ``$INCLUDE``
853    will raise a ``SyntaxError`` exception.
854
855    *check_origin*, a ``bool``.  If ``True``, the default, then sanity
856    checks of the origin node will be made by calling the zone's
857    ``check_origin()`` method.
858
859    *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
860    encoder/decoder.  If ``None``, the default IDNA 2003 encoder/decoder
861    is used.
862
863    Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
864
865    Raises ``dns.zone.NoNS`` if there is no NS RRset.
866
867    Raises ``KeyError`` if there is no origin node.
868
869    Returns a subclass of ``dns.zone.Zone``.
870    """
871
872    with contextlib.ExitStack() as stack:
873        if isinstance(f, str):
874            if filename is None:
875                filename = f
876            f = stack.enter_context(open(f))
877        return from_text(f, origin, rdclass, relativize, zone_factory,
878                         filename, allow_include, check_origin)
879
880
881def from_xfr(xfr, zone_factory=Zone, relativize=True, check_origin=True):
882    """Convert the output of a zone transfer generator into a zone object.
883
884    *xfr*, a generator of ``dns.message.Message`` objects, typically
885    ``dns.query.xfr()``.
886
887    *relativize*, a ``bool``, determine's whether domain names are
888    relativized to the zone's origin.  The default is ``True``.
889    It is essential that the relativize setting matches the one specified
890    to the generator.
891
892    *check_origin*, a ``bool``.  If ``True``, the default, then sanity
893    checks of the origin node will be made by calling the zone's
894    ``check_origin()`` method.
895
896    Raises ``dns.zone.NoSOA`` if there is no SOA RRset.
897
898    Raises ``dns.zone.NoNS`` if there is no NS RRset.
899
900    Raises ``KeyError`` if there is no origin node.
901
902    Returns a subclass of ``dns.zone.Zone``.
903    """
904
905    z = None
906    for r in xfr:
907        if z is None:
908            if relativize:
909                origin = r.origin
910            else:
911                origin = r.answer[0].name
912            rdclass = r.answer[0].rdclass
913            z = zone_factory(origin, rdclass, relativize=relativize)
914        for rrset in r.answer:
915            znode = z.nodes.get(rrset.name)
916            if not znode:
917                znode = z.node_factory()
918                z.nodes[rrset.name] = znode
919            zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype,
920                                       rrset.covers, True)
921            zrds.update_ttl(rrset.ttl)
922            for rd in rrset:
923                zrds.add(rd)
924    if check_origin:
925        z.check_origin()
926    return z
927