1# Copyright (C) 2003-2007, 2009, 2010 Nominum, Inc.
2#
3# Permission to use, copy, modify, and distribute this software and its
4# documentation for any purpose with or without fee is hereby granted,
5# provided that the above copyright notice and this permission notice
6# appear in all copies.
7#
8# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
9# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
11# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
14# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16"""DNS Zones."""
17
18from __future__ import generators
19
20import sys
21
22import dns.exception
23import dns.name
24import dns.node
25import dns.rdataclass
26import dns.rdatatype
27import dns.rdata
28import dns.rrset
29import dns.tokenizer
30import dns.ttl
31
32class BadZone(dns.exception.DNSException):
33    """The zone is malformed."""
34    pass
35
36class NoSOA(BadZone):
37    """The zone has no SOA RR at its origin."""
38    pass
39
40class NoNS(BadZone):
41    """The zone has no NS RRset at its origin."""
42    pass
43
44class UnknownOrigin(BadZone):
45    """The zone's origin is unknown."""
46    pass
47
48class Zone(object):
49    """A DNS zone.
50
51    A Zone is a mapping from names to nodes.  The zone object may be
52    treated like a Python dictionary, e.g. zone[name] will retrieve
53    the node associated with that name.  The I{name} may be a
54    dns.name.Name object, or it may be a string.  In the either case,
55    if the name is relative it is treated as relative to the origin of
56    the zone.
57
58    @ivar rdclass: The zone's rdata class; the default is class IN.
59    @type rdclass: int
60    @ivar origin: The origin of the zone.
61    @type origin: dns.name.Name object
62    @ivar nodes: A dictionary mapping the names of nodes in the zone to the
63    nodes themselves.
64    @type nodes: dict
65    @ivar relativize: should names in the zone be relativized?
66    @type relativize: bool
67    @cvar node_factory: the factory used to create a new node
68    @type node_factory: class or callable
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        @param origin: The origin of the zone.
79        @type origin: dns.name.Name object
80        @param rdclass: The zone's rdata class; the default is class IN.
81        @type rdclass: int"""
82
83        self.rdclass = rdclass
84        self.origin = origin
85        self.nodes = {}
86        self.relativize = relativize
87
88    def __eq__(self, other):
89        """Two zones are equal if they have the same origin, class, and
90        nodes.
91        @rtype: bool
92        """
93
94        if not isinstance(other, Zone):
95            return False
96        if self.rdclass != other.rdclass or \
97           self.origin != other.origin or \
98           self.nodes != other.nodes:
99            return False
100        return True
101
102    def __ne__(self, other):
103        """Are two zones not equal?
104        @rtype: bool
105        """
106
107        return not self.__eq__(other)
108
109    def _validate_name(self, name):
110        if isinstance(name, (str, unicode)):
111            name = dns.name.from_text(name, None)
112        elif not isinstance(name, dns.name.Name):
113            raise KeyError("name parameter must be convertable to a DNS name")
114        if name.is_absolute():
115            if not name.is_subdomain(self.origin):
116                raise KeyError("name parameter must be a subdomain of the zone origin")
117            if self.relativize:
118                name = name.relativize(self.origin)
119        return name
120
121    def __getitem__(self, key):
122        key = self._validate_name(key)
123        return self.nodes[key]
124
125    def __setitem__(self, key, value):
126        key = self._validate_name(key)
127        self.nodes[key] = value
128
129    def __delitem__(self, key):
130        key = self._validate_name(key)
131        del self.nodes[key]
132
133    def __iter__(self):
134        return self.nodes.iterkeys()
135
136    def iterkeys(self):
137        return self.nodes.iterkeys()
138
139    def keys(self):
140        return self.nodes.keys()
141
142    def itervalues(self):
143        return self.nodes.itervalues()
144
145    def values(self):
146        return self.nodes.values()
147
148    def iteritems(self):
149        return self.nodes.iteritems()
150
151    def items(self):
152        return self.nodes.items()
153
154    def get(self, key):
155        key = self._validate_name(key)
156        return self.nodes.get(key)
157
158    def __contains__(self, other):
159        return other in self.nodes
160
161    def find_node(self, name, create=False):
162        """Find a node in the zone, possibly creating it.
163
164        @param name: the name of the node to find
165        @type name: dns.name.Name object or string
166        @param create: should the node be created if it doesn't exist?
167        @type create: bool
168        @raises KeyError: the name is not known and create was not specified.
169        @rtype: dns.node.Node object
170        """
171
172        name = self._validate_name(name)
173        node = self.nodes.get(name)
174        if node is None:
175            if not create:
176                raise KeyError
177            node = self.node_factory()
178            self.nodes[name] = node
179        return node
180
181    def get_node(self, name, create=False):
182        """Get a node in the zone, possibly creating it.
183
184        This method is like L{find_node}, except it returns None instead
185        of raising an exception if the node does not exist and creation
186        has not been requested.
187
188        @param name: the name of the node to find
189        @type name: dns.name.Name object or string
190        @param create: should the node be created if it doesn't exist?
191        @type create: bool
192        @rtype: dns.node.Node object or None
193        """
194
195        try:
196            node = self.find_node(name, create)
197        except KeyError:
198            node = None
199        return node
200
201    def delete_node(self, name):
202        """Delete the specified node if it exists.
203
204        It is not an error if the node does not exist.
205        """
206
207        name = self._validate_name(name)
208        if self.nodes.has_key(name):
209            del self.nodes[name]
210
211    def find_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,
212                      create=False):
213        """Look for rdata with the specified name and type in the zone,
214        and return an rdataset encapsulating it.
215
216        The I{name}, I{rdtype}, and I{covers} parameters may be
217        strings, in which case they will be converted to their proper
218        type.
219
220        The rdataset returned is not a copy; changes to it will change
221        the zone.
222
223        KeyError is raised if the name or type are not found.
224        Use L{get_rdataset} if you want to have None returned instead.
225
226        @param name: the owner name to look for
227        @type name: DNS.name.Name object or string
228        @param rdtype: the rdata type desired
229        @type rdtype: int or string
230        @param covers: the covered type (defaults to None)
231        @type covers: int or string
232        @param create: should the node and rdataset be created if they do not
233        exist?
234        @type create: bool
235        @raises KeyError: the node or rdata could not be found
236        @rtype: dns.rrset.RRset object
237        """
238
239        name = self._validate_name(name)
240        if isinstance(rdtype, str):
241            rdtype = dns.rdatatype.from_text(rdtype)
242        if isinstance(covers, str):
243            covers = dns.rdatatype.from_text(covers)
244        node = self.find_node(name, create)
245        return node.find_rdataset(self.rdclass, rdtype, covers, create)
246
247    def get_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,
248                     create=False):
249        """Look for rdata with the specified name and type in the zone,
250        and return an rdataset encapsulating it.
251
252        The I{name}, I{rdtype}, and I{covers} parameters may be
253        strings, in which case they will be converted to their proper
254        type.
255
256        The rdataset returned is not a copy; changes to it will change
257        the zone.
258
259        None is returned if the name or type are not found.
260        Use L{find_rdataset} if you want to have KeyError raised instead.
261
262        @param name: the owner name to look for
263        @type name: DNS.name.Name object or string
264        @param rdtype: the rdata type desired
265        @type rdtype: int or string
266        @param covers: the covered type (defaults to None)
267        @type covers: int or string
268        @param create: should the node and rdataset be created if they do not
269        exist?
270        @type create: bool
271        @rtype: dns.rrset.RRset object
272        """
273
274        try:
275            rdataset = self.find_rdataset(name, rdtype, covers, create)
276        except KeyError:
277            rdataset = None
278        return rdataset
279
280    def delete_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE):
281        """Delete the rdataset matching I{rdtype} and I{covers}, if it
282        exists at the node specified by I{name}.
283
284        The I{name}, I{rdtype}, and I{covers} parameters may be
285        strings, in which case they will be converted to their proper
286        type.
287
288        It is not an error if the node does not exist, or if there is no
289        matching rdataset at the node.
290
291        If the node has no rdatasets after the deletion, it will itself
292        be deleted.
293
294        @param name: the owner name to look for
295        @type name: DNS.name.Name object or string
296        @param rdtype: the rdata type desired
297        @type rdtype: int or string
298        @param covers: the covered type (defaults to None)
299        @type covers: int or string
300        """
301
302        name = self._validate_name(name)
303        if isinstance(rdtype, str):
304            rdtype = dns.rdatatype.from_text(rdtype)
305        if isinstance(covers, str):
306            covers = dns.rdatatype.from_text(covers)
307        node = self.get_node(name)
308        if not node is None:
309            node.delete_rdataset(self.rdclass, rdtype, covers)
310            if len(node) == 0:
311                self.delete_node(name)
312
313    def replace_rdataset(self, name, replacement):
314        """Replace an rdataset at name.
315
316        It is not an error if there is no rdataset matching I{replacement}.
317
318        Ownership of the I{replacement} object is transferred to the zone;
319        in other words, this method does not store a copy of I{replacement}
320        at the node, it stores I{replacement} itself.
321
322        If the I{name} node does not exist, it is created.
323
324        @param name: the owner name
325        @type name: DNS.name.Name object or string
326        @param replacement: the replacement rdataset
327        @type replacement: dns.rdataset.Rdataset
328        """
329
330        if replacement.rdclass != self.rdclass:
331            raise ValueError('replacement.rdclass != zone.rdclass')
332        node = self.find_node(name, True)
333        node.replace_rdataset(replacement)
334
335    def find_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):
336        """Look for rdata with the specified name and type in the zone,
337        and return an RRset encapsulating it.
338
339        The I{name}, I{rdtype}, and I{covers} parameters may be
340        strings, in which case they will be converted to their proper
341        type.
342
343        This method is less efficient than the similar
344        L{find_rdataset} because it creates an RRset instead of
345        returning the matching rdataset.  It may be more convenient
346        for some uses since it returns an object which binds the owner
347        name to the rdata.
348
349        This method may not be used to create new nodes or rdatasets;
350        use L{find_rdataset} instead.
351
352        KeyError is raised if the name or type are not found.
353        Use L{get_rrset} if you want to have None returned instead.
354
355        @param name: the owner name to look for
356        @type name: DNS.name.Name object or string
357        @param rdtype: the rdata type desired
358        @type rdtype: int or string
359        @param covers: the covered type (defaults to None)
360        @type covers: int or string
361        @raises KeyError: the node or rdata could not be found
362        @rtype: dns.rrset.RRset object
363        """
364
365        name = self._validate_name(name)
366        if isinstance(rdtype, str):
367            rdtype = dns.rdatatype.from_text(rdtype)
368        if isinstance(covers, str):
369            covers = dns.rdatatype.from_text(covers)
370        rdataset = self.nodes[name].find_rdataset(self.rdclass, rdtype, covers)
371        rrset = dns.rrset.RRset(name, self.rdclass, rdtype, covers)
372        rrset.update(rdataset)
373        return rrset
374
375    def get_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):
376        """Look for rdata with the specified name and type in the zone,
377        and return an RRset encapsulating it.
378
379        The I{name}, I{rdtype}, and I{covers} parameters may be
380        strings, in which case they will be converted to their proper
381        type.
382
383        This method is less efficient than the similar L{get_rdataset}
384        because it creates an RRset instead of returning the matching
385        rdataset.  It may be more convenient for some uses since it
386        returns an object which binds the owner name to the rdata.
387
388        This method may not be used to create new nodes or rdatasets;
389        use L{find_rdataset} instead.
390
391        None is returned if the name or type are not found.
392        Use L{find_rrset} if you want to have KeyError raised instead.
393
394        @param name: the owner name to look for
395        @type name: DNS.name.Name object or string
396        @param rdtype: the rdata type desired
397        @type rdtype: int or string
398        @param covers: the covered type (defaults to None)
399        @type covers: int or string
400        @rtype: dns.rrset.RRset object
401        """
402
403        try:
404            rrset = self.find_rrset(name, rdtype, covers)
405        except KeyError:
406            rrset = None
407        return rrset
408
409    def iterate_rdatasets(self, rdtype=dns.rdatatype.ANY,
410                          covers=dns.rdatatype.NONE):
411        """Return a generator which yields (name, rdataset) tuples for
412        all rdatasets in the zone which have the specified I{rdtype}
413        and I{covers}.  If I{rdtype} is dns.rdatatype.ANY, the default,
414        then all rdatasets will be matched.
415
416        @param rdtype: int or string
417        @type rdtype: int or string
418        @param covers: the covered type (defaults to None)
419        @type covers: int or string
420        """
421
422        if isinstance(rdtype, str):
423            rdtype = dns.rdatatype.from_text(rdtype)
424        if isinstance(covers, str):
425            covers = dns.rdatatype.from_text(covers)
426        for (name, node) in self.iteritems():
427            for rds in node:
428                if rdtype == dns.rdatatype.ANY or \
429                   (rds.rdtype == rdtype and rds.covers == covers):
430                    yield (name, rds)
431
432    def iterate_rdatas(self, rdtype=dns.rdatatype.ANY,
433                       covers=dns.rdatatype.NONE):
434        """Return a generator which yields (name, ttl, rdata) tuples for
435        all rdatas in the zone which have the specified I{rdtype}
436        and I{covers}.  If I{rdtype} is dns.rdatatype.ANY, the default,
437        then all rdatas will be matched.
438
439        @param rdtype: int or string
440        @type rdtype: int or string
441        @param covers: the covered type (defaults to None)
442        @type covers: int or string
443        """
444
445        if isinstance(rdtype, str):
446            rdtype = dns.rdatatype.from_text(rdtype)
447        if isinstance(covers, str):
448            covers = dns.rdatatype.from_text(covers)
449        for (name, node) in self.iteritems():
450            for rds in node:
451                if rdtype == dns.rdatatype.ANY or \
452                   (rds.rdtype == rdtype and rds.covers == covers):
453                    for rdata in rds:
454                        yield (name, rds.ttl, rdata)
455
456    def to_file(self, f, sorted=True, relativize=True, nl=None):
457        """Write a zone to a file.
458
459        @param f: file or string.  If I{f} is a string, it is treated
460        as the name of a file to open.
461        @param sorted: if True, the file will be written with the
462        names sorted in DNSSEC order from least to greatest.  Otherwise
463        the names will be written in whatever order they happen to have
464        in the zone's dictionary.
465        @param relativize: if True, domain names in the output will be
466        relativized to the zone's origin (if possible).
467        @type relativize: bool
468        @param nl: The end of line string.  If not specified, the
469        output will use the platform's native end-of-line marker (i.e.
470        LF on POSIX, CRLF on Windows, CR on Macintosh).
471        @type nl: string or None
472        """
473
474        if sys.hexversion >= 0x02030000:
475            # allow Unicode filenames
476            str_type = basestring
477        else:
478            str_type = str
479        if nl is None:
480            opts = 'w'
481        else:
482            opts = 'wb'
483        if isinstance(f, str_type):
484            f = file(f, opts)
485            want_close = True
486        else:
487            want_close = False
488        try:
489            if sorted:
490                names = self.keys()
491                names.sort()
492            else:
493                names = self.iterkeys()
494            for n in names:
495                l = self[n].to_text(n, origin=self.origin,
496                                    relativize=relativize)
497                if nl is None:
498                    print >> f, l
499                else:
500                    f.write(l)
501                    f.write(nl)
502        finally:
503            if want_close:
504                f.close()
505
506    def check_origin(self):
507        """Do some simple checking of the zone's origin.
508
509        @raises dns.zone.NoSOA: there is no SOA RR
510        @raises dns.zone.NoNS: there is no NS RRset
511        @raises KeyError: there is no origin node
512        """
513        if self.relativize:
514            name = dns.name.empty
515        else:
516            name = self.origin
517        if self.get_rdataset(name, dns.rdatatype.SOA) is None:
518            raise NoSOA
519        if self.get_rdataset(name, dns.rdatatype.NS) is None:
520            raise NoNS
521
522
523class _MasterReader(object):
524    """Read a DNS master file
525
526    @ivar tok: The tokenizer
527    @type tok: dns.tokenizer.Tokenizer object
528    @ivar ttl: The default TTL
529    @type ttl: int
530    @ivar last_name: The last name read
531    @type last_name: dns.name.Name object
532    @ivar current_origin: The current origin
533    @type current_origin: dns.name.Name object
534    @ivar relativize: should names in the zone be relativized?
535    @type relativize: bool
536    @ivar zone: the zone
537    @type zone: dns.zone.Zone object
538    @ivar saved_state: saved reader state (used when processing $INCLUDE)
539    @type saved_state: list of (tokenizer, current_origin, last_name, file)
540    tuples.
541    @ivar current_file: the file object of the $INCLUDed file being parsed
542    (None if no $INCLUDE is active).
543    @ivar allow_include: is $INCLUDE allowed?
544    @type allow_include: bool
545    @ivar check_origin: should sanity checks of the origin node be done?
546    The default is True.
547    @type check_origin: bool
548    """
549
550    def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone,
551                 allow_include=False, check_origin=True):
552        if isinstance(origin, (str, unicode)):
553            origin = dns.name.from_text(origin)
554        self.tok = tok
555        self.current_origin = origin
556        self.relativize = relativize
557        self.ttl = 0
558        self.last_name = None
559        self.zone = zone_factory(origin, rdclass, relativize=relativize)
560        self.saved_state = []
561        self.current_file = None
562        self.allow_include = allow_include
563        self.check_origin = check_origin
564
565    def _eat_line(self):
566        while 1:
567            token = self.tok.get()
568            if token.is_eol_or_eof():
569                break
570
571    def _rr_line(self):
572        """Process one line from a DNS master file."""
573        # Name
574        if self.current_origin is None:
575            raise UnknownOrigin
576        token = self.tok.get(want_leading = True)
577        if not token.is_whitespace():
578            self.last_name = dns.name.from_text(token.value, self.current_origin)
579        else:
580            token = self.tok.get()
581            if token.is_eol_or_eof():
582                # treat leading WS followed by EOL/EOF as if they were EOL/EOF.
583                return
584            self.tok.unget(token)
585        name = self.last_name
586        if not name.is_subdomain(self.zone.origin):
587            self._eat_line()
588            return
589        if self.relativize:
590            name = name.relativize(self.zone.origin)
591        token = self.tok.get()
592        if not token.is_identifier():
593            raise dns.exception.SyntaxError
594        # TTL
595        try:
596            ttl = dns.ttl.from_text(token.value)
597            token = self.tok.get()
598            if not token.is_identifier():
599                raise dns.exception.SyntaxError
600        except dns.ttl.BadTTL:
601            ttl = self.ttl
602        # Class
603        try:
604            rdclass = dns.rdataclass.from_text(token.value)
605            token = self.tok.get()
606            if not token.is_identifier():
607                raise dns.exception.SyntaxError
608        except dns.exception.SyntaxError:
609            raise dns.exception.SyntaxError
610        except:
611            rdclass = self.zone.rdclass
612        if rdclass != self.zone.rdclass:
613            raise dns.exception.SyntaxError("RR class is not zone's class")
614        # Type
615        try:
616            rdtype = dns.rdatatype.from_text(token.value)
617        except:
618            raise dns.exception.SyntaxError("unknown rdatatype '%s'" % token.value)
619        n = self.zone.nodes.get(name)
620        if n is None:
621            n = self.zone.node_factory()
622            self.zone.nodes[name] = n
623        try:
624            rd = dns.rdata.from_text(rdclass, rdtype, self.tok,
625                                     self.current_origin, False)
626        except dns.exception.SyntaxError:
627            # Catch and reraise.
628            (ty, va) = sys.exc_info()[:2]
629            raise va
630        except:
631            # All exceptions that occur in the processing of rdata
632            # are treated as syntax errors.  This is not strictly
633            # correct, but it is correct almost all of the time.
634            # We convert them to syntax errors so that we can emit
635            # helpful filename:line info.
636            (ty, va) = sys.exc_info()[:2]
637            raise dns.exception.SyntaxError("caught exception %s: %s" % (str(ty), str(va)))
638
639        rd.choose_relativity(self.zone.origin, self.relativize)
640        covers = rd.covers()
641        rds = n.find_rdataset(rdclass, rdtype, covers, True)
642        rds.add(rd, ttl)
643
644    def read(self):
645        """Read a DNS master file and build a zone object.
646
647        @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
648        @raises dns.zone.NoNS: No NS RRset was found at the zone origin
649        """
650
651        try:
652            while 1:
653                token = self.tok.get(True, True).unescape()
654                if token.is_eof():
655                    if not self.current_file is None:
656                        self.current_file.close()
657                    if len(self.saved_state) > 0:
658                        (self.tok,
659                         self.current_origin,
660                         self.last_name,
661                         self.current_file,
662                         self.ttl) = self.saved_state.pop(-1)
663                        continue
664                    break
665                elif token.is_eol():
666                    continue
667                elif token.is_comment():
668                    self.tok.get_eol()
669                    continue
670                elif token.value[0] == '$':
671                    u = token.value.upper()
672                    if u == '$TTL':
673                        token = self.tok.get()
674                        if not token.is_identifier():
675                            raise dns.exception.SyntaxError("bad $TTL")
676                        self.ttl = dns.ttl.from_text(token.value)
677                        self.tok.get_eol()
678                    elif u == '$ORIGIN':
679                        self.current_origin = self.tok.get_name()
680                        self.tok.get_eol()
681                        if self.zone.origin is None:
682                            self.zone.origin = self.current_origin
683                    elif u == '$INCLUDE' and self.allow_include:
684                        token = self.tok.get()
685                        if not token.is_quoted_string():
686                            raise dns.exception.SyntaxError("bad filename in $INCLUDE")
687                        filename = token.value
688                        token = self.tok.get()
689                        if token.is_identifier():
690                            new_origin = dns.name.from_text(token.value, \
691                                                            self.current_origin)
692                            self.tok.get_eol()
693                        elif not token.is_eol_or_eof():
694                            raise dns.exception.SyntaxError("bad origin in $INCLUDE")
695                        else:
696                            new_origin = self.current_origin
697                        self.saved_state.append((self.tok,
698                                                 self.current_origin,
699                                                 self.last_name,
700                                                 self.current_file,
701                                                 self.ttl))
702                        self.current_file = file(filename, 'r')
703                        self.tok = dns.tokenizer.Tokenizer(self.current_file,
704                                                           filename)
705                        self.current_origin = new_origin
706                    else:
707                        raise dns.exception.SyntaxError("Unknown master file directive '" + u + "'")
708                    continue
709                self.tok.unget(token)
710                self._rr_line()
711        except dns.exception.SyntaxError, detail:
712            (filename, line_number) = self.tok.where()
713            if detail is None:
714                detail = "syntax error"
715            raise dns.exception.SyntaxError("%s:%d: %s" % (filename, line_number, detail))
716
717        # Now that we're done reading, do some basic checking of the zone.
718        if self.check_origin:
719            self.zone.check_origin()
720
721def from_text(text, origin = None, rdclass = dns.rdataclass.IN,
722              relativize = True, zone_factory=Zone, filename=None,
723              allow_include=False, check_origin=True):
724    """Build a zone object from a master file format string.
725
726    @param text: the master file format input
727    @type text: string.
728    @param origin: The origin of the zone; if not specified, the first
729    $ORIGIN statement in the master file will determine the origin of the
730    zone.
731    @type origin: dns.name.Name object or string
732    @param rdclass: The zone's rdata class; the default is class IN.
733    @type rdclass: int
734    @param relativize: should names be relativized?  The default is True
735    @type relativize: bool
736    @param zone_factory: The zone factory to use
737    @type zone_factory: function returning a Zone
738    @param filename: The filename to emit when describing where an error
739    occurred; the default is '<string>'.
740    @type filename: string
741    @param allow_include: is $INCLUDE allowed?
742    @type allow_include: bool
743    @param check_origin: should sanity checks of the origin node be done?
744    The default is True.
745    @type check_origin: bool
746    @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
747    @raises dns.zone.NoNS: No NS RRset was found at the zone origin
748    @rtype: dns.zone.Zone object
749    """
750
751    # 'text' can also be a file, but we don't publish that fact
752    # since it's an implementation detail.  The official file
753    # interface is from_file().
754
755    if filename is None:
756        filename = '<string>'
757    tok = dns.tokenizer.Tokenizer(text, filename)
758    reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory,
759                           allow_include=allow_include,
760                           check_origin=check_origin)
761    reader.read()
762    return reader.zone
763
764def from_file(f, origin = None, rdclass = dns.rdataclass.IN,
765              relativize = True, zone_factory=Zone, filename=None,
766              allow_include=True, check_origin=True):
767    """Read a master file and build a zone object.
768
769    @param f: file or string.  If I{f} is a string, it is treated
770    as the name of a file to open.
771    @param origin: The origin of the zone; if not specified, the first
772    $ORIGIN statement in the master file will determine the origin of the
773    zone.
774    @type origin: dns.name.Name object or string
775    @param rdclass: The zone's rdata class; the default is class IN.
776    @type rdclass: int
777    @param relativize: should names be relativized?  The default is True
778    @type relativize: bool
779    @param zone_factory: The zone factory to use
780    @type zone_factory: function returning a Zone
781    @param filename: The filename to emit when describing where an error
782    occurred; the default is '<file>', or the value of I{f} if I{f} is a
783    string.
784    @type filename: string
785    @param allow_include: is $INCLUDE allowed?
786    @type allow_include: bool
787    @param check_origin: should sanity checks of the origin node be done?
788    The default is True.
789    @type check_origin: bool
790    @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
791    @raises dns.zone.NoNS: No NS RRset was found at the zone origin
792    @rtype: dns.zone.Zone object
793    """
794
795    if sys.hexversion >= 0x02030000:
796        # allow Unicode filenames; turn on universal newline support
797        str_type = basestring
798        opts = 'rU'
799    else:
800        str_type = str
801        opts = 'r'
802    if isinstance(f, str_type):
803        if filename is None:
804            filename = f
805        f = file(f, opts)
806        want_close = True
807    else:
808        if filename is None:
809            filename = '<file>'
810        want_close = False
811
812    try:
813        z = from_text(f, origin, rdclass, relativize, zone_factory,
814                      filename, allow_include, check_origin)
815    finally:
816        if want_close:
817            f.close()
818    return z
819
820def from_xfr(xfr, zone_factory=Zone, relativize=True):
821    """Convert the output of a zone transfer generator into a zone object.
822
823    @param xfr: The xfr generator
824    @type xfr: generator of dns.message.Message objects
825    @param relativize: should names be relativized?  The default is True.
826    It is essential that the relativize setting matches the one specified
827    to dns.query.xfr().
828    @type relativize: bool
829    @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
830    @raises dns.zone.NoNS: No NS RRset was found at the zone origin
831    @rtype: dns.zone.Zone object
832    """
833
834    z = None
835    for r in xfr:
836        if z is None:
837            if relativize:
838                origin = r.origin
839            else:
840                origin = r.answer[0].name
841            rdclass = r.answer[0].rdclass
842            z = zone_factory(origin, rdclass, relativize=relativize)
843        for rrset in r.answer:
844            znode = z.nodes.get(rrset.name)
845            if not znode:
846                znode = z.node_factory()
847                z.nodes[rrset.name] = znode
848            zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype,
849                                       rrset.covers, True)
850            zrds.update_ttl(rrset.ttl)
851            for rd in rrset:
852                rd.choose_relativity(z.origin, relativize)
853                zrds.add(rd)
854    z.check_origin()
855    return z
856