1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
37 """The zone has no SOA RR at its origin."""
38 pass
39
41 """The zone has no NS RRset at its origin."""
42 pass
43
45 """The zone's origin is unknown."""
46 pass
47
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
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
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
103 """Are two zones not equal?
104 @rtype: bool
105 """
106
107 return not self.__eq__(other)
108
122
126
130
134
137
140
143
146
149
152
155
156 - def get(self, key):
159
161 return other in self.nodes
162
164 """Find a node in the zone, possibly creating it.
165
166 @param name: the name of the node to find
167 @type name: dns.name.Name object or string
168 @param create: should the node be created if it doesn't exist?
169 @type create: bool
170 @raises KeyError: the name is not known and create was not specified.
171 @rtype: dns.node.Node object
172 """
173
174 name = self._validate_name(name)
175 node = self.nodes.get(name)
176 if node is None:
177 if not create:
178 raise KeyError
179 node = self.node_factory()
180 self.nodes[name] = node
181 return node
182
183 - def get_node(self, name, create=False):
184 """Get a node in the zone, possibly creating it.
185
186 This method is like L{find_node}, except it returns None instead
187 of raising an exception if the node does not exist and creation
188 has not been requested.
189
190 @param name: the name of the node to find
191 @type name: dns.name.Name object or string
192 @param create: should the node be created if it doesn't exist?
193 @type create: bool
194 @rtype: dns.node.Node object or None
195 """
196
197 try:
198 node = self.find_node(name, create)
199 except KeyError:
200 node = None
201 return node
202
204 """Delete the specified node if it exists.
205
206 It is not an error if the node does not exist.
207 """
208
209 name = self._validate_name(name)
210 if self.nodes.has_key(name):
211 del self.nodes[name]
212
215 """Look for rdata with the specified name and type in the zone,
216 and return an rdataset encapsulating it.
217
218 The I{name}, I{rdtype}, and I{covers} parameters may be
219 strings, in which case they will be converted to their proper
220 type.
221
222 The rdataset returned is not a copy; changes to it will change
223 the zone.
224
225 KeyError is raised if the name or type are not found.
226 Use L{get_rdataset} if you want to have None returned instead.
227
228 @param name: the owner name to look for
229 @type name: DNS.name.Name object or string
230 @param rdtype: the rdata type desired
231 @type rdtype: int or string
232 @param covers: the covered type (defaults to None)
233 @type covers: int or string
234 @param create: should the node and rdataset be created if they do not
235 exist?
236 @type create: bool
237 @raises KeyError: the node or rdata could not be found
238 @rtype: dns.rrset.RRset object
239 """
240
241 name = self._validate_name(name)
242 if isinstance(rdtype, str):
243 rdtype = dns.rdatatype.from_text(rdtype)
244 if isinstance(covers, str):
245 covers = dns.rdatatype.from_text(covers)
246 node = self.find_node(name, create)
247 return node.find_rdataset(self.rdclass, rdtype, covers, create)
248
251 """Look for rdata with the specified name and type in the zone,
252 and return an rdataset encapsulating it.
253
254 The I{name}, I{rdtype}, and I{covers} parameters may be
255 strings, in which case they will be converted to their proper
256 type.
257
258 The rdataset returned is not a copy; changes to it will change
259 the zone.
260
261 None is returned if the name or type are not found.
262 Use L{find_rdataset} if you want to have KeyError raised instead.
263
264 @param name: the owner name to look for
265 @type name: DNS.name.Name object or string
266 @param rdtype: the rdata type desired
267 @type rdtype: int or string
268 @param covers: the covered type (defaults to None)
269 @type covers: int or string
270 @param create: should the node and rdataset be created if they do not
271 exist?
272 @type create: bool
273 @rtype: dns.rrset.RRset object
274 """
275
276 try:
277 rdataset = self.find_rdataset(name, rdtype, covers, create)
278 except KeyError:
279 rdataset = None
280 return rdataset
281
283 """Delete the rdataset matching I{rdtype} and I{covers}, if it
284 exists at the node specified by I{name}.
285
286 The I{name}, I{rdtype}, and I{covers} parameters may be
287 strings, in which case they will be converted to their proper
288 type.
289
290 It is not an error if the node does not exist, or if there is no
291 matching rdataset at the node.
292
293 If the node has no rdatasets after the deletion, it will itself
294 be deleted.
295
296 @param name: the owner name to look for
297 @type name: DNS.name.Name object or string
298 @param rdtype: the rdata type desired
299 @type rdtype: int or string
300 @param covers: the covered type (defaults to None)
301 @type covers: int or string
302 """
303
304 name = self._validate_name(name)
305 if isinstance(rdtype, str):
306 rdtype = dns.rdatatype.from_text(rdtype)
307 if isinstance(covers, str):
308 covers = dns.rdatatype.from_text(covers)
309 node = self.get_node(name)
310 if not node is None:
311 node.delete_rdataset(self.rdclass, rdtype, covers)
312 if len(node) == 0:
313 self.delete_node(name)
314
316 """Replace an rdataset at name.
317
318 It is not an error if there is no rdataset matching I{replacement}.
319
320 Ownership of the I{replacement} object is transferred to the zone;
321 in other words, this method does not store a copy of I{replacement}
322 at the node, it stores I{replacement} itself.
323
324 If the I{name} node does not exist, it is created.
325
326 @param name: the owner name
327 @type name: DNS.name.Name object or string
328 @param replacement: the replacement rdataset
329 @type replacement: dns.rdataset.Rdataset
330 """
331
332 if replacement.rdclass != self.rdclass:
333 raise ValueError, 'replacement.rdclass != zone.rdclass'
334 node = self.find_node(name, True)
335 node.replace_rdataset(replacement)
336
338 """Look for rdata with the specified name and type in the zone,
339 and return an RRset encapsulating it.
340
341 The I{name}, I{rdtype}, and I{covers} parameters may be
342 strings, in which case they will be converted to their proper
343 type.
344
345 This method is less efficient than the similar
346 L{find_rdataset} because it creates an RRset instead of
347 returning the matching rdataset. It may be more convenient
348 for some uses since it returns an object which binds the owner
349 name to the rdata.
350
351 This method may not be used to create new nodes or rdatasets;
352 use L{find_rdataset} instead.
353
354 KeyError is raised if the name or type are not found.
355 Use L{get_rrset} if you want to have None returned instead.
356
357 @param name: the owner name to look for
358 @type name: DNS.name.Name object or string
359 @param rdtype: the rdata type desired
360 @type rdtype: int or string
361 @param covers: the covered type (defaults to None)
362 @type covers: int or string
363 @raises KeyError: the node or rdata could not be found
364 @rtype: dns.rrset.RRset object
365 """
366
367 name = self._validate_name(name)
368 if isinstance(rdtype, str):
369 rdtype = dns.rdatatype.from_text(rdtype)
370 if isinstance(covers, str):
371 covers = dns.rdatatype.from_text(covers)
372 rdataset = self.nodes[name].find_rdataset(self.rdclass, rdtype, covers)
373 rrset = dns.rrset.RRset(name, self.rdclass, rdtype, covers)
374 rrset.update(rdataset)
375 return rrset
376
378 """Look for rdata with the specified name and type in the zone,
379 and return an RRset encapsulating it.
380
381 The I{name}, I{rdtype}, and I{covers} parameters may be
382 strings, in which case they will be converted to their proper
383 type.
384
385 This method is less efficient than the similar L{get_rdataset}
386 because it creates an RRset instead of returning the matching
387 rdataset. It may be more convenient for some uses since it
388 returns an object which binds the owner name to the rdata.
389
390 This method may not be used to create new nodes or rdatasets;
391 use L{find_rdataset} instead.
392
393 None is returned if the name or type are not found.
394 Use L{find_rrset} if you want to have KeyError raised instead.
395
396 @param name: the owner name to look for
397 @type name: DNS.name.Name object or string
398 @param rdtype: the rdata type desired
399 @type rdtype: int or string
400 @param covers: the covered type (defaults to None)
401 @type covers: int or string
402 @rtype: dns.rrset.RRset object
403 """
404
405 try:
406 rrset = self.find_rrset(name, rdtype, covers)
407 except KeyError:
408 rrset = None
409 return rrset
410
433
457
458 - def to_file(self, f, sorted=True, relativize=True, nl=None):
459 """Write a zone to a file.
460
461 @param f: file or string. If I{f} is a string, it is treated
462 as the name of a file to open.
463 @param sorted: if True, the file will be written with the
464 names sorted in DNSSEC order from least to greatest. Otherwise
465 the names will be written in whatever order they happen to have
466 in the zone's dictionary.
467 @param relativize: if True, domain names in the output will be
468 relativized to the zone's origin (if possible).
469 @type relativize: bool
470 @param nl: The end of line string. If not specified, the
471 output will use the platform's native end-of-line marker (i.e.
472 LF on POSIX, CRLF on Windows, CR on Macintosh).
473 @type nl: string or None
474 """
475
476 if sys.hexversion >= 0x02030000:
477
478 str_type = basestring
479 else:
480 str_type = str
481 if nl is None:
482 opts = 'w'
483 else:
484 opts = 'wb'
485 if isinstance(f, str_type):
486 f = file(f, opts)
487 want_close = True
488 else:
489 want_close = False
490 try:
491 if sorted:
492 names = self.keys()
493 names.sort()
494 else:
495 names = self.iterkeys()
496 for n in names:
497 l = self[n].to_text(n, origin=self.origin,
498 relativize=relativize)
499 if nl is None:
500 print >> f, l
501 else:
502 f.write(l)
503 f.write(nl)
504 finally:
505 if want_close:
506 f.close()
507
523
524
526 """Read a DNS master file
527
528 @ivar tok: The tokenizer
529 @type tok: dns.tokenizer.Tokenizer object
530 @ivar ttl: The default TTL
531 @type ttl: int
532 @ivar last_name: The last name read
533 @type last_name: dns.name.Name object
534 @ivar current_origin: The current origin
535 @type current_origin: dns.name.Name object
536 @ivar relativize: should names in the zone be relativized?
537 @type relativize: bool
538 @ivar zone: the zone
539 @type zone: dns.zone.Zone object
540 @ivar saved_state: saved reader state (used when processing $INCLUDE)
541 @type saved_state: list of (tokenizer, current_origin, last_name, file)
542 tuples.
543 @ivar current_file: the file object of the $INCLUDed file being parsed
544 (None if no $INCLUDE is active).
545 @ivar allow_include: is $INCLUDE allowed?
546 @type allow_include: bool
547 @ivar check_origin: should sanity checks of the origin node be done?
548 The default is True.
549 @type check_origin: bool
550 """
551
552 - def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone,
553 allow_include=False, check_origin=True):
566
572
574 """Process one line from a DNS master file."""
575
576 if self.current_origin is None:
577 raise UnknownOrigin
578 token = self.tok.get(want_leading = True)
579 if token[0] != dns.tokenizer.WHITESPACE:
580 self.last_name = dns.name.from_text(token[1], self.current_origin)
581 else:
582 token = self.tok.get()
583 if token[0] == dns.tokenizer.EOL or \
584 token[0] == dns.tokenizer.EOF:
585
586 return
587 self.tok.unget(token)
588 name = self.last_name
589 if not name.is_subdomain(self.zone.origin):
590 self._eat_line()
591 return
592 if self.relativize:
593 name = name.relativize(self.zone.origin)
594 token = self.tok.get()
595 if token[0] != dns.tokenizer.IDENTIFIER:
596 raise dns.exception.SyntaxError
597
598 try:
599 ttl = dns.ttl.from_text(token[1])
600 token = self.tok.get()
601 if token[0] != dns.tokenizer.IDENTIFIER:
602 raise dns.exception.SyntaxError
603 except dns.ttl.BadTTL:
604 ttl = self.ttl
605
606 try:
607 rdclass = dns.rdataclass.from_text(token[1])
608 token = self.tok.get()
609 if token[0] != dns.tokenizer.IDENTIFIER:
610 raise dns.exception.SyntaxError
611 except dns.exception.SyntaxError:
612 raise dns.exception.SyntaxError
613 except:
614 rdclass = self.zone.rdclass
615 if rdclass != self.zone.rdclass:
616 raise dns.exception.SyntaxError, "RR class is not zone's class"
617
618 try:
619 rdtype = dns.rdatatype.from_text(token[1])
620 except:
621 raise dns.exception.SyntaxError, \
622 "unknown rdatatype '%s'" % token[1]
623 n = self.zone.nodes.get(name)
624 if n is None:
625 n = self.zone.node_factory()
626 self.zone.nodes[name] = n
627 try:
628 rd = dns.rdata.from_text(rdclass, rdtype, self.tok,
629 self.current_origin, False)
630 except dns.exception.SyntaxError:
631
632 (ty, va) = sys.exc_info()[:2]
633 raise ty, va
634 except:
635
636
637
638
639
640
641 (ty, va) = sys.exc_info()[:2]
642 raise dns.exception.SyntaxError, \
643 "caught exception %s: %s" % (str(ty), str(va))
644
645 rd.choose_relativity(self.zone.origin, self.relativize)
646 covers = rd.covers()
647 rds = n.find_rdataset(rdclass, rdtype, covers, True)
648 rds.add(rd, ttl)
649
651 """Read a DNS master file and build a zone object.
652
653 @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
654 @raises dns.zone.NoNS: No NS RRset was found at the zone origin
655 """
656
657 try:
658 while 1:
659 token = self.tok.get(True, True)
660 if token[0] == dns.tokenizer.EOF:
661 if not self.current_file is None:
662 self.current_file.close()
663 if len(self.saved_state) > 0:
664 (self.tok,
665 self.current_origin,
666 self.last_name,
667 self.current_file,
668 self.ttl) = self.saved_state.pop(-1)
669 continue
670 break
671 elif token[0] == dns.tokenizer.EOL:
672 continue
673 elif token[0] == dns.tokenizer.COMMENT:
674 self.tok.get_eol()
675 continue
676 elif token[1][0] == '$':
677 u = token[1].upper()
678 if u == '$TTL':
679 token = self.tok.get()
680 if token[0] != dns.tokenizer.IDENTIFIER:
681 raise dns.exception.SyntaxError, "bad $TTL"
682 self.ttl = dns.ttl.from_text(token[1])
683 self.tok.get_eol()
684 elif u == '$ORIGIN':
685 self.current_origin = self.tok.get_name()
686 self.tok.get_eol()
687 if self.zone.origin is None:
688 self.zone.origin = self.current_origin
689 elif u == '$INCLUDE' and self.allow_include:
690 token = self.tok.get()
691 if token[0] != dns.tokenizer.QUOTED_STRING:
692 raise dns.exception.SyntaxError, \
693 "bad filename in $INCLUDE"
694 filename = token[1]
695 token = self.tok.get()
696 if token[0] == dns.tokenizer.IDENTIFIER:
697 new_origin = dns.name.from_text(token[1], \
698 self.current_origin)
699 self.tok.get_eol()
700 elif token[0] != dns.tokenizer.EOL and \
701 token[0] != dns.tokenizer.EOF:
702 raise dns.exception.SyntaxError, \
703 "bad origin in $INCLUDE"
704 else:
705 new_origin = self.current_origin
706 self.saved_state.append((self.tok,
707 self.current_origin,
708 self.last_name,
709 self.current_file,
710 self.ttl))
711 self.current_file = file(filename, 'r')
712 self.tok = dns.tokenizer.Tokenizer(self.current_file,
713 filename)
714 self.current_origin = new_origin
715 else:
716 raise dns.exception.SyntaxError, \
717 "Unknown master file directive '" + u + "'"
718 continue
719 self.tok.unget(token)
720 self._rr_line()
721 except dns.exception.SyntaxError, detail:
722 (filename, line_number) = self.tok.where()
723 if detail is None:
724 detail = "syntax error"
725 raise dns.exception.SyntaxError, \
726 "%s:%d: %s" % (filename, line_number, detail)
727
728
729 if self.check_origin:
730 self.zone.check_origin()
731
732 -def from_text(text, origin = None, rdclass = dns.rdataclass.IN,
733 relativize = True, zone_factory=Zone, filename=None,
734 allow_include=False, check_origin=True):
735 """Build a zone object from a master file format string.
736
737 @param text: the master file format input
738 @type text: string.
739 @param origin: The origin of the zone; if not specified, the first
740 $ORIGIN statement in the master file will determine the origin of the
741 zone.
742 @type origin: dns.name.Name object or string
743 @param rdclass: The zone's rdata class; the default is class IN.
744 @type rdclass: int
745 @param relativize: should names be relativized? The default is True
746 @type relativize: bool
747 @param zone_factory: The zone factory to use
748 @type zone_factory: function returning a Zone
749 @param filename: The filename to emit when describing where an error
750 occurred; the default is '<string>'.
751 @type filename: string
752 @param allow_include: is $INCLUDE allowed?
753 @type allow_include: bool
754 @param check_origin: should sanity checks of the origin node be done?
755 The default is True.
756 @type check_origin: bool
757 @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
758 @raises dns.zone.NoNS: No NS RRset was found at the zone origin
759 @rtype: dns.zone.Zone object
760 """
761
762
763
764
765
766 if filename is None:
767 filename = '<string>'
768 tok = dns.tokenizer.Tokenizer(text, filename)
769 reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory,
770 allow_include=allow_include,
771 check_origin=check_origin)
772 reader.read()
773 return reader.zone
774
775 -def from_file(f, origin = None, rdclass = dns.rdataclass.IN,
776 relativize = True, zone_factory=Zone, filename=None,
777 allow_include=True, check_origin=True):
778 """Read a master file and build a zone object.
779
780 @param f: file or string. If I{f} is a string, it is treated
781 as the name of a file to open.
782 @param origin: The origin of the zone; if not specified, the first
783 $ORIGIN statement in the master file will determine the origin of the
784 zone.
785 @type origin: dns.name.Name object or string
786 @param rdclass: The zone's rdata class; the default is class IN.
787 @type rdclass: int
788 @param relativize: should names be relativized? The default is True
789 @type relativize: bool
790 @param zone_factory: The zone factory to use
791 @type zone_factory: function returning a Zone
792 @param filename: The filename to emit when describing where an error
793 occurred; the default is '<file>', or the value of I{f} if I{f} is a
794 string.
795 @type filename: string
796 @param allow_include: is $INCLUDE allowed?
797 @type allow_include: bool
798 @param check_origin: should sanity checks of the origin node be done?
799 The default is True.
800 @type check_origin: bool
801 @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
802 @raises dns.zone.NoNS: No NS RRset was found at the zone origin
803 @rtype: dns.zone.Zone object
804 """
805
806 if sys.hexversion >= 0x02030000:
807
808 str_type = basestring
809 opts = 'rU'
810 else:
811 str_type = str
812 opts = 'r'
813 if isinstance(f, str_type):
814 if filename is None:
815 filename = f
816 f = file(f, opts)
817 want_close = True
818 else:
819 if filename is None:
820 filename = '<file>'
821 want_close = False
822
823 try:
824 z = from_text(f, origin, rdclass, relativize, zone_factory,
825 filename, allow_include, check_origin)
826 finally:
827 if want_close:
828 f.close()
829 return z
830
867