dnspython: Update to newer upstream snapshot.
[samba.git] / lib / dnspython / dns / zone.py
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
18 from __future__ import generators
19
20 import sys
21
22 import dns.exception
23 import dns.name
24 import dns.node
25 import dns.rdataclass
26 import dns.rdatatype
27 import dns.rdata
28 import dns.rrset
29 import dns.tokenizer
30 import dns.ttl
31
32 class BadZone(dns.exception.DNSException):
33     """The zone is malformed."""
34     pass
35
36 class NoSOA(BadZone):
37     """The zone has no SOA RR at its origin."""
38     pass
39
40 class NoNS(BadZone):
41     """The zone has no NS RRset at its origin."""
42     pass
43
44 class UnknownOrigin(BadZone):
45     """The zone's origin is unknown."""
46     pass
47
48 class 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, unicode)):
241             rdtype = dns.rdatatype.from_text(rdtype)
242         if isinstance(covers, (str, unicode)):
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, unicode)):
304             rdtype = dns.rdatatype.from_text(rdtype)
305         if isinstance(covers, (str, unicode)):
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, unicode)):
367             rdtype = dns.rdatatype.from_text(rdtype)
368         if isinstance(covers, (str, unicode)):
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, unicode)):
423             rdtype = dns.rdatatype.from_text(rdtype)
424         if isinstance(covers, (str, unicode)):
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, unicode)):
446             rdtype = dns.rdatatype.from_text(rdtype)
447         if isinstance(covers, (str, unicode)):
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
523 class _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
721 def 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
764 def 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
820 def 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