1 # Copyright (C) 2003-2007, 2009, 2010 Nominum, Inc.
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.
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.
18 from __future__
import generators
32 class BadZone(dns
.exception
.DNSException
):
33 """The zone is malformed."""
37 """The zone has no SOA RR at its origin."""
41 """The zone has no NS RRset at its origin."""
44 class UnknownOrigin(BadZone
):
45 """The zone's origin is unknown."""
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
58 @ivar rdclass: The zone's rdata class; the default is class IN.
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
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
71 node_factory
= dns
.node
.Node
73 __slots__
= ['rdclass', 'origin', 'nodes', 'relativize']
75 def __init__(self
, origin
, rdclass
=dns
.rdataclass
.IN
, relativize
=True):
76 """Initialize a zone object.
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.
83 self
.rdclass
= rdclass
86 self
.relativize
= relativize
88 def __eq__(self
, other
):
89 """Two zones are equal if they have the same origin, class, and
94 if not isinstance(other
, Zone
):
96 if self
.rdclass
!= other
.rdclass
or \
97 self
.origin
!= other
.origin
or \
98 self
.nodes
!= other
.nodes
:
102 def __ne__(self
, other
):
103 """Are two zones not equal?
107 return not self
.__eq
__(other
)
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")
118 name
= name
.relativize(self
.origin
)
121 def __getitem__(self
, key
):
122 key
= self
._validate
_name
(key
)
123 return self
.nodes
[key
]
125 def __setitem__(self
, key
, value
):
126 key
= self
._validate
_name
(key
)
127 self
.nodes
[key
] = value
129 def __delitem__(self
, key
):
130 key
= self
._validate
_name
(key
)
134 return self
.nodes
.iterkeys()
137 return self
.nodes
.iterkeys()
140 return self
.nodes
.keys()
142 def itervalues(self
):
143 return self
.nodes
.itervalues()
146 return self
.nodes
.values()
149 return self
.nodes
.iteritems()
152 return self
.nodes
.items()
155 key
= self
._validate
_name
(key
)
156 return self
.nodes
.get(key
)
158 def __contains__(self
, other
):
159 return other
in self
.nodes
161 def find_node(self
, name
, create
=False):
162 """Find a node in the zone, possibly creating it.
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?
168 @raises KeyError: the name is not known and create was not specified.
169 @rtype: dns.node.Node object
172 name
= self
._validate
_name
(name
)
173 node
= self
.nodes
.get(name
)
177 node
= self
.node_factory()
178 self
.nodes
[name
] = node
181 def get_node(self
, name
, create
=False):
182 """Get a node in the zone, possibly creating it.
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.
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?
192 @rtype: dns.node.Node object or None
196 node
= self
.find_node(name
, create
)
201 def delete_node(self
, name
):
202 """Delete the specified node if it exists.
204 It is not an error if the node does not exist.
207 name
= self
._validate
_name
(name
)
208 if self
.nodes
.has_key(name
):
211 def find_rdataset(self
, name
, rdtype
, covers
=dns
.rdatatype
.NONE
,
213 """Look for rdata with the specified name and type in the zone,
214 and return an rdataset encapsulating it.
216 The I{name}, I{rdtype}, and I{covers} parameters may be
217 strings, in which case they will be converted to their proper
220 The rdataset returned is not a copy; changes to it will change
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.
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
235 @raises KeyError: the node or rdata could not be found
236 @rtype: dns.rrset.RRset object
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
)
247 def get_rdataset(self
, name
, rdtype
, covers
=dns
.rdatatype
.NONE
,
249 """Look for rdata with the specified name and type in the zone,
250 and return an rdataset encapsulating it.
252 The I{name}, I{rdtype}, and I{covers} parameters may be
253 strings, in which case they will be converted to their proper
256 The rdataset returned is not a copy; changes to it will change
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.
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
271 @rtype: dns.rrset.RRset object
275 rdataset
= self
.find_rdataset(name
, rdtype
, covers
, create
)
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}.
284 The I{name}, I{rdtype}, and I{covers} parameters may be
285 strings, in which case they will be converted to their proper
288 It is not an error if the node does not exist, or if there is no
289 matching rdataset at the node.
291 If the node has no rdatasets after the deletion, it will itself
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
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
)
309 node
.delete_rdataset(self
.rdclass
, rdtype
, covers
)
311 self
.delete_node(name
)
313 def replace_rdataset(self
, name
, replacement
):
314 """Replace an rdataset at name.
316 It is not an error if there is no rdataset matching I{replacement}.
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.
322 If the I{name} node does not exist, it is created.
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
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
)
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.
339 The I{name}, I{rdtype}, and I{covers} parameters may be
340 strings, in which case they will be converted to their proper
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
349 This method may not be used to create new nodes or rdatasets;
350 use L{find_rdataset} instead.
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.
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
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
)
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.
379 The I{name}, I{rdtype}, and I{covers} parameters may be
380 strings, in which case they will be converted to their proper
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.
388 This method may not be used to create new nodes or rdatasets;
389 use L{find_rdataset} instead.
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.
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
404 rrset
= self
.find_rrset(name
, rdtype
, covers
)
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.
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
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():
428 if rdtype
== dns
.rdatatype
.ANY
or \
429 (rds
.rdtype
== rdtype
and rds
.covers
== covers
):
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.
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
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():
451 if rdtype
== dns
.rdatatype
.ANY
or \
452 (rds
.rdtype
== rdtype
and rds
.covers
== covers
):
454 yield (name
, rds
.ttl
, rdata
)
456 def to_file(self
, f
, sorted=True, relativize
=True, nl
=None):
457 """Write a zone to a file.
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
474 if sys
.hexversion
>= 0x02030000:
475 # allow Unicode filenames
476 str_type
= basestring
483 if isinstance(f
, str_type
):
493 names
= self
.iterkeys()
495 l
= self
[n
].to_text(n
, origin
=self
.origin
,
496 relativize
=relativize
)
506 def check_origin(self
):
507 """Do some simple checking of the zone's origin.
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
514 name
= dns
.name
.empty
517 if self
.get_rdataset(name
, dns
.rdatatype
.SOA
) is None:
519 if self
.get_rdataset(name
, dns
.rdatatype
.NS
) is None:
523 class _MasterReader(object):
524 """Read a DNS master file
526 @ivar tok: The tokenizer
527 @type tok: dns.tokenizer.Tokenizer object
528 @ivar ttl: The default TTL
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
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)
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?
547 @type check_origin: bool
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
)
555 self
.current_origin
= origin
556 self
.relativize
= relativize
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
567 token
= self
.tok
.get()
568 if token
.is_eol_or_eof():
572 """Process one line from a DNS master file."""
574 if self
.current_origin
is None:
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
)
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.
584 self
.tok
.unget(token
)
585 name
= self
.last_name
586 if not name
.is_subdomain(self
.zone
.origin
):
590 name
= name
.relativize(self
.zone
.origin
)
591 token
= self
.tok
.get()
592 if not token
.is_identifier():
593 raise dns
.exception
.SyntaxError
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
:
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
611 rdclass
= self
.zone
.rdclass
612 if rdclass
!= self
.zone
.rdclass
:
613 raise dns
.exception
.SyntaxError("RR class is not zone's class")
616 rdtype
= dns
.rdatatype
.from_text(token
.value
)
618 raise dns
.exception
.SyntaxError("unknown rdatatype '%s'" % token
.value
)
619 n
= self
.zone
.nodes
.get(name
)
621 n
= self
.zone
.node_factory()
622 self
.zone
.nodes
[name
] = n
624 rd
= dns
.rdata
.from_text(rdclass
, rdtype
, self
.tok
,
625 self
.current_origin
, False)
626 except dns
.exception
.SyntaxError:
628 (ty
, va
) = sys
.exc_info()[:2]
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
)))
639 rd
.choose_relativity(self
.zone
.origin
, self
.relativize
)
641 rds
= n
.find_rdataset(rdclass
, rdtype
, covers
, True)
645 """Read a DNS master file and build a zone object.
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
653 token
= self
.tok
.get(True, True).unescape()
655 if not self
.current_file
is None:
656 self
.current_file
.close()
657 if len(self
.saved_state
) > 0:
662 self
.ttl
) = self
.saved_state
.pop(-1)
667 elif token
.is_comment():
670 elif token
.value
[0] == '$':
671 u
= token
.value
.upper()
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
)
679 self
.current_origin
= self
.tok
.get_name()
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
, \
693 elif not token
.is_eol_or_eof():
694 raise dns
.exception
.SyntaxError("bad origin in $INCLUDE")
696 new_origin
= self
.current_origin
697 self
.saved_state
.append((self
.tok
,
702 self
.current_file
= file(filename
, 'r')
703 self
.tok
= dns
.tokenizer
.Tokenizer(self
.current_file
,
705 self
.current_origin
= new_origin
707 raise dns
.exception
.SyntaxError("Unknown master file directive '" + u
+ "'")
709 self
.tok
.unget(token
)
711 except dns
.exception
.SyntaxError, detail
:
712 (filename
, line_number
) = self
.tok
.where()
714 detail
= "syntax error"
715 raise dns
.exception
.SyntaxError("%s:%d: %s" % (filename
, line_number
, detail
))
717 # Now that we're done reading, do some basic checking of the zone.
718 if self
.check_origin
:
719 self
.zone
.check_origin()
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.
726 @param text: the master file format input
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
731 @type origin: dns.name.Name object or string
732 @param rdclass: The zone's rdata class; the default is class IN.
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?
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
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().
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
)
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.
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
774 @type origin: dns.name.Name object or string
775 @param rdclass: The zone's rdata class; the default is class IN.
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
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?
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
795 if sys
.hexversion
>= 0x02030000:
796 # allow Unicode filenames; turn on universal newline support
797 str_type
= basestring
802 if isinstance(f
, str_type
):
813 z
= from_text(f
, origin
, rdclass
, relativize
, zone_factory
,
814 filename
, allow_include
, check_origin
)
820 def from_xfr(xfr
, zone_factory
=Zone
, relativize
=True):
821 """Convert the output of a zone transfer generator into a zone object.
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
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
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
)
846 znode
= z
.node_factory()
847 z
.nodes
[rrset
.name
] = znode
848 zrds
= znode
.find_rdataset(rrset
.rdclass
, rrset
.rdtype
,
850 zrds
.update_ttl(rrset
.ttl
)
852 rd
.choose_relativity(z
.origin
, relativize
)