dnspython  1.16.0
About: dnspython is a DNS toolkit (for Python 2.x) that supports almost all record types.
  Fossies Dox: dnspython-1.16.0.tar.gz  ("inofficial" and yet experimental doxygen-generated source code documentation)  

zone.py
Go to the documentation of this file.
1 # Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
2 
3 # Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
4 #
5 # Permission to use, copy, modify, and distribute this software and its
6 # documentation for any purpose with or without fee is hereby granted,
7 # provided that the above copyright notice and this permission notice
8 # appear in all copies.
9 #
10 # THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
11 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
13 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
16 # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 
18 """DNS Zones."""
19 
20 from __future__ import generators
21 
22 import sys
23 import re
24 import os
25 from io import BytesIO
26 
27 import dns.exception
28 import dns.name
29 import dns.node
30 import dns.rdataclass
31 import dns.rdatatype
32 import dns.rdata
34 import dns.rrset
35 import dns.tokenizer
36 import dns.ttl
37 import dns.grange
38 from ._compat import string_types, text_type, PY3
39 
40 
42 
43  """The DNS zone is malformed."""
44 
45 
46 class NoSOA(BadZone):
47 
48  """The DNS zone has no SOA RR at its origin."""
49 
50 
51 class NoNS(BadZone):
52 
53  """The DNS zone has no NS RRset at its origin."""
54 
55 
57 
58  """The DNS zone's origin is unknown."""
59 
60 
61 class Zone(object):
62 
63  """A DNS zone.
64 
65  A Zone is a mapping from names to nodes. The zone object may be
66  treated like a Python dictionary, e.g. zone[name] will retrieve
67  the node associated with that name. The I{name} may be a
68  dns.name.Name object, or it may be a string. In the either case,
69  if the name is relative it is treated as relative to the origin of
70  the zone.
71 
72  @ivar rdclass: The zone's rdata class; the default is class IN.
73  @type rdclass: int
74  @ivar origin: The origin of the zone.
75  @type origin: dns.name.Name object
76  @ivar nodes: A dictionary mapping the names of nodes in the zone to the
77  nodes themselves.
78  @type nodes: dict
79  @ivar relativize: should names in the zone be relativized?
80  @type relativize: bool
81  @cvar node_factory: the factory used to create a new node
82  @type node_factory: class or callable
83  """
84 
85  node_factory = dns.node.Node
86 
87  __slots__ = ['rdclass', 'origin', 'nodes', 'relativize']
88 
89  def __init__(self, origin, rdclass=dns.rdataclass.IN, relativize=True):
90  """Initialize a zone object.
91 
92  @param origin: The origin of the zone.
93  @type origin: dns.name.Name object
94  @param rdclass: The zone's rdata class; the default is class IN.
95  @type rdclass: int"""
96 
97  if origin is not None:
98  if isinstance(origin, string_types):
99  origin = dns.name.from_text(origin)
100  elif not isinstance(origin, dns.name.Name):
101  raise ValueError("origin parameter must be convertible to a "
102  "DNS name")
103  if not origin.is_absolute():
104  raise ValueError("origin parameter must be an absolute name")
105  self.origin = origin
106  self.rdclass = rdclass
107  self.nodes = {}
108  self.relativize = relativize
109 
110  def __eq__(self, other):
111  """Two zones are equal if they have the same origin, class, and
112  nodes.
113  @rtype: bool
114  """
115 
116  if not isinstance(other, Zone):
117  return False
118  if self.rdclass != other.rdclass or \
119  self.origin != other.origin or \
120  self.nodes != other.nodes:
121  return False
122  return True
123 
124  def __ne__(self, other):
125  """Are two zones not equal?
126  @rtype: bool
127  """
128 
129  return not self.__eq__(other)
130 
131  def _validate_name(self, name):
132  if isinstance(name, string_types):
133  name = dns.name.from_text(name, None)
134  elif not isinstance(name, dns.name.Name):
135  raise KeyError("name parameter must be convertible to a DNS name")
136  if name.is_absolute():
137  if not name.is_subdomain(self.origin):
138  raise KeyError(
139  "name parameter must be a subdomain of the zone origin")
140  if self.relativize:
141  name = name.relativize(self.origin)
142  return name
143 
144  def __getitem__(self, key):
145  key = self._validate_name(key)
146  return self.nodes[key]
147 
148  def __setitem__(self, key, value):
149  key = self._validate_name(key)
150  self.nodes[key] = value
151 
152  def __delitem__(self, key):
153  key = self._validate_name(key)
154  del self.nodes[key]
155 
156  def __iter__(self):
157  return self.nodes.__iter__()
158 
159  def iterkeys(self):
160  if PY3:
161  return self.nodes.keys() # pylint: disable=dict-keys-not-iterating
162  else:
163  return self.nodes.iterkeys() # pylint: disable=dict-iter-method
164 
165  def keys(self):
166  return self.nodes.keys() # pylint: disable=dict-keys-not-iterating
167 
168  def itervalues(self):
169  if PY3:
170  return self.nodes.values() # pylint: disable=dict-values-not-iterating
171  else:
172  return self.nodes.itervalues() # pylint: disable=dict-iter-method
173 
174  def values(self):
175  return self.nodes.values() # pylint: disable=dict-values-not-iterating
176 
177  def items(self):
178  return self.nodes.items() # pylint: disable=dict-items-not-iterating
179 
180  iteritems = items
181 
182  def get(self, key):
183  key = self._validate_name(key)
184  return self.nodes.get(key)
185 
186  def __contains__(self, other):
187  return other in self.nodes
188 
189  def find_node(self, name, create=False):
190  """Find a node in the zone, possibly creating it.
191 
192  @param name: the name of the node to find
193  @type name: dns.name.Name object or string
194  @param create: should the node be created if it doesn't exist?
195  @type create: bool
196  @raises KeyError: the name is not known and create was not specified.
197  @rtype: dns.node.Node object
198  """
199 
200  name = self._validate_name(name)
201  node = self.nodes.get(name)
202  if node is None:
203  if not create:
204  raise KeyError
205  node = self.node_factory()
206  self.nodes[name] = node
207  return node
208 
209  def get_node(self, name, create=False):
210  """Get a node in the zone, possibly creating it.
211 
212  This method is like L{find_node}, except it returns None instead
213  of raising an exception if the node does not exist and creation
214  has not been requested.
215 
216  @param name: the name of the node to find
217  @type name: dns.name.Name object or string
218  @param create: should the node be created if it doesn't exist?
219  @type create: bool
220  @rtype: dns.node.Node object or None
221  """
222 
223  try:
224  node = self.find_node(name, create)
225  except KeyError:
226  node = None
227  return node
228 
229  def delete_node(self, name):
230  """Delete the specified node if it exists.
231 
232  It is not an error if the node does not exist.
233  """
234 
235  name = self._validate_name(name)
236  if name in self.nodes:
237  del self.nodes[name]
238 
239  def find_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,
240  create=False):
241  """Look for rdata with the specified name and type in the zone,
242  and return an rdataset encapsulating it.
243 
244  The I{name}, I{rdtype}, and I{covers} parameters may be
245  strings, in which case they will be converted to their proper
246  type.
247 
248  The rdataset returned is not a copy; changes to it will change
249  the zone.
250 
251  KeyError is raised if the name or type are not found.
252  Use L{get_rdataset} if you want to have None returned instead.
253 
254  @param name: the owner name to look for
255  @type name: DNS.name.Name object or string
256  @param rdtype: the rdata type desired
257  @type rdtype: int or string
258  @param covers: the covered type (defaults to None)
259  @type covers: int or string
260  @param create: should the node and rdataset be created if they do not
261  exist?
262  @type create: bool
263  @raises KeyError: the node or rdata could not be found
264  @rtype: dns.rdataset.Rdataset object
265  """
266 
267  name = self._validate_name(name)
268  if isinstance(rdtype, string_types):
269  rdtype = dns.rdatatype.from_text(rdtype)
270  if isinstance(covers, string_types):
271  covers = dns.rdatatype.from_text(covers)
272  node = self.find_node(name, create)
273  return node.find_rdataset(self.rdclass, rdtype, covers, create)
274 
275  def get_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,
276  create=False):
277  """Look for rdata with the specified name and type in the zone,
278  and return an rdataset encapsulating it.
279 
280  The I{name}, I{rdtype}, and I{covers} parameters may be
281  strings, in which case they will be converted to their proper
282  type.
283 
284  The rdataset returned is not a copy; changes to it will change
285  the zone.
286 
287  None is returned if the name or type are not found.
288  Use L{find_rdataset} if you want to have KeyError raised instead.
289 
290  @param name: the owner name to look for
291  @type name: DNS.name.Name object or string
292  @param rdtype: the rdata type desired
293  @type rdtype: int or string
294  @param covers: the covered type (defaults to None)
295  @type covers: int or string
296  @param create: should the node and rdataset be created if they do not
297  exist?
298  @type create: bool
299  @rtype: dns.rdataset.Rdataset object or None
300  """
301 
302  try:
303  rdataset = self.find_rdataset(name, rdtype, covers, create)
304  except KeyError:
305  rdataset = None
306  return rdataset
307 
308  def delete_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE):
309  """Delete the rdataset matching I{rdtype} and I{covers}, if it
310  exists at the node specified by I{name}.
311 
312  The I{name}, I{rdtype}, and I{covers} parameters may be
313  strings, in which case they will be converted to their proper
314  type.
315 
316  It is not an error if the node does not exist, or if there is no
317  matching rdataset at the node.
318 
319  If the node has no rdatasets after the deletion, it will itself
320  be deleted.
321 
322  @param name: the owner name to look for
323  @type name: DNS.name.Name object or string
324  @param rdtype: the rdata type desired
325  @type rdtype: int or string
326  @param covers: the covered type (defaults to None)
327  @type covers: int or string
328  """
329 
330  name = self._validate_name(name)
331  if isinstance(rdtype, string_types):
332  rdtype = dns.rdatatype.from_text(rdtype)
333  if isinstance(covers, string_types):
334  covers = dns.rdatatype.from_text(covers)
335  node = self.get_node(name)
336  if node is not None:
337  node.delete_rdataset(self.rdclass, rdtype, covers)
338  if len(node) == 0:
339  self.delete_node(name)
340 
341  def replace_rdataset(self, name, replacement):
342  """Replace an rdataset at name.
343 
344  It is not an error if there is no rdataset matching I{replacement}.
345 
346  Ownership of the I{replacement} object is transferred to the zone;
347  in other words, this method does not store a copy of I{replacement}
348  at the node, it stores I{replacement} itself.
349 
350  If the I{name} node does not exist, it is created.
351 
352  @param name: the owner name
353  @type name: DNS.name.Name object or string
354  @param replacement: the replacement rdataset
355  @type replacement: dns.rdataset.Rdataset
356  """
357 
358  if replacement.rdclass != self.rdclass:
359  raise ValueError('replacement.rdclass != zone.rdclass')
360  node = self.find_node(name, True)
361  node.replace_rdataset(replacement)
362 
363  def find_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):
364  """Look for rdata with the specified name and type in the zone,
365  and return an RRset encapsulating it.
366 
367  The I{name}, I{rdtype}, and I{covers} parameters may be
368  strings, in which case they will be converted to their proper
369  type.
370 
371  This method is less efficient than the similar
372  L{find_rdataset} because it creates an RRset instead of
373  returning the matching rdataset. It may be more convenient
374  for some uses since it returns an object which binds the owner
375  name to the rdata.
376 
377  This method may not be used to create new nodes or rdatasets;
378  use L{find_rdataset} instead.
379 
380  KeyError is raised if the name or type are not found.
381  Use L{get_rrset} if you want to have None returned instead.
382 
383  @param name: the owner name to look for
384  @type name: DNS.name.Name object or string
385  @param rdtype: the rdata type desired
386  @type rdtype: int or string
387  @param covers: the covered type (defaults to None)
388  @type covers: int or string
389  @raises KeyError: the node or rdata could not be found
390  @rtype: dns.rrset.RRset object
391  """
392 
393  name = self._validate_name(name)
394  if isinstance(rdtype, string_types):
395  rdtype = dns.rdatatype.from_text(rdtype)
396  if isinstance(covers, string_types):
397  covers = dns.rdatatype.from_text(covers)
398  rdataset = self.nodes[name].find_rdataset(self.rdclass, rdtype, covers)
399  rrset = dns.rrset.RRset(name, self.rdclass, rdtype, covers)
400  rrset.update(rdataset)
401  return rrset
402 
403  def get_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):
404  """Look for rdata with the specified name and type in the zone,
405  and return an RRset encapsulating it.
406 
407  The I{name}, I{rdtype}, and I{covers} parameters may be
408  strings, in which case they will be converted to their proper
409  type.
410 
411  This method is less efficient than the similar L{get_rdataset}
412  because it creates an RRset instead of returning the matching
413  rdataset. It may be more convenient for some uses since it
414  returns an object which binds the owner name to the rdata.
415 
416  This method may not be used to create new nodes or rdatasets;
417  use L{find_rdataset} instead.
418 
419  None is returned if the name or type are not found.
420  Use L{find_rrset} if you want to have KeyError raised instead.
421 
422  @param name: the owner name to look for
423  @type name: DNS.name.Name object or string
424  @param rdtype: the rdata type desired
425  @type rdtype: int or string
426  @param covers: the covered type (defaults to None)
427  @type covers: int or string
428  @rtype: dns.rrset.RRset object
429  """
430 
431  try:
432  rrset = self.find_rrset(name, rdtype, covers)
433  except KeyError:
434  rrset = None
435  return rrset
436 
437  def iterate_rdatasets(self, rdtype=dns.rdatatype.ANY,
438  covers=dns.rdatatype.NONE):
439  """Return a generator which yields (name, rdataset) tuples for
440  all rdatasets in the zone which have the specified I{rdtype}
441  and I{covers}. If I{rdtype} is dns.rdatatype.ANY, the default,
442  then all rdatasets will be matched.
443 
444  @param rdtype: int or string
445  @type rdtype: int or string
446  @param covers: the covered type (defaults to None)
447  @type covers: int or string
448  """
449 
450  if isinstance(rdtype, string_types):
451  rdtype = dns.rdatatype.from_text(rdtype)
452  if isinstance(covers, string_types):
453  covers = dns.rdatatype.from_text(covers)
454  for (name, node) in self.iteritems(): # pylint: disable=dict-iter-method
455  for rds in node:
456  if rdtype == dns.rdatatype.ANY or \
457  (rds.rdtype == rdtype and rds.covers == covers):
458  yield (name, rds)
459 
460  def iterate_rdatas(self, rdtype=dns.rdatatype.ANY,
461  covers=dns.rdatatype.NONE):
462  """Return a generator which yields (name, ttl, rdata) tuples for
463  all rdatas in the zone which have the specified I{rdtype}
464  and I{covers}. If I{rdtype} is dns.rdatatype.ANY, the default,
465  then all rdatas will be matched.
466 
467  @param rdtype: int or string
468  @type rdtype: int or string
469  @param covers: the covered type (defaults to None)
470  @type covers: int or string
471  """
472 
473  if isinstance(rdtype, string_types):
474  rdtype = dns.rdatatype.from_text(rdtype)
475  if isinstance(covers, string_types):
476  covers = dns.rdatatype.from_text(covers)
477  for (name, node) in self.iteritems(): # pylint: disable=dict-iter-method
478  for rds in node:
479  if rdtype == dns.rdatatype.ANY or \
480  (rds.rdtype == rdtype and rds.covers == covers):
481  for rdata in rds:
482  yield (name, rds.ttl, rdata)
483 
484  def to_file(self, f, sorted=True, relativize=True, nl=None):
485  """Write a zone to a file.
486 
487  @param f: file or string. If I{f} is a string, it is treated
488  as the name of a file to open.
489  @param sorted: if True, the file will be written with the
490  names sorted in DNSSEC order from least to greatest. Otherwise
491  the names will be written in whatever order they happen to have
492  in the zone's dictionary.
493  @param relativize: if True, domain names in the output will be
494  relativized to the zone's origin (if possible).
495  @type relativize: bool
496  @param nl: The end of line string. If not specified, the
497  output will use the platform's native end-of-line marker (i.e.
498  LF on POSIX, CRLF on Windows, CR on Macintosh).
499  @type nl: string or None
500  """
501 
502  if isinstance(f, string_types):
503  f = open(f, 'wb')
504  want_close = True
505  else:
506  want_close = False
507 
508  # must be in this way, f.encoding may contain None, or even attribute
509  # may not be there
510  file_enc = getattr(f, 'encoding', None)
511  if file_enc is None:
512  file_enc = 'utf-8'
513 
514  if nl is None:
515  nl_b = os.linesep.encode(file_enc) # binary mode, '\n' is not enough
516  nl = u'\n'
517  elif isinstance(nl, string_types):
518  nl_b = nl.encode(file_enc)
519  else:
520  nl_b = nl
521  nl = nl.decode()
522 
523  try:
524  if sorted:
525  names = list(self.keys())
526  names.sort()
527  else:
528  names = self.iterkeys() # pylint: disable=dict-iter-method
529  for n in names:
530  l = self[n].to_text(n, origin=self.origin,
531  relativize=relativize)
532  if isinstance(l, text_type):
533  l_b = l.encode(file_enc)
534  else:
535  l_b = l
536  l = l.decode()
537 
538  try:
539  f.write(l_b)
540  f.write(nl_b)
541  except TypeError: # textual mode
542  f.write(l)
543  f.write(nl)
544  finally:
545  if want_close:
546  f.close()
547 
548  def to_text(self, sorted=True, relativize=True, nl=None):
549  """Return a zone's text as though it were written to a file.
550 
551  @param sorted: if True, the file will be written with the
552  names sorted in DNSSEC order from least to greatest. Otherwise
553  the names will be written in whatever order they happen to have
554  in the zone's dictionary.
555  @param relativize: if True, domain names in the output will be
556  relativized to the zone's origin (if possible).
557  @type relativize: bool
558  @param nl: The end of line string. If not specified, the
559  output will use the platform's native end-of-line marker (i.e.
560  LF on POSIX, CRLF on Windows, CR on Macintosh).
561  @type nl: string or None
562  """
563  temp_buffer = BytesIO()
564  self.to_file(temp_buffer, sorted, relativize, nl)
565  return_value = temp_buffer.getvalue()
566  temp_buffer.close()
567  return return_value
568 
569  def check_origin(self):
570  """Do some simple checking of the zone's origin.
571 
572  @raises dns.zone.NoSOA: there is no SOA RR
573  @raises dns.zone.NoNS: there is no NS RRset
574  @raises KeyError: there is no origin node
575  """
576  if self.relativize:
577  name = dns.name.empty
578  else:
579  name = self.origin
580  if self.get_rdataset(name, dns.rdatatype.SOA) is None:
581  raise NoSOA
582  if self.get_rdataset(name, dns.rdatatype.NS) is None:
583  raise NoNS
584 
585 
586 class _MasterReader(object):
587 
588  """Read a DNS master file
589 
590  @ivar tok: The tokenizer
591  @type tok: dns.tokenizer.Tokenizer object
592  @ivar last_ttl: The last seen explicit TTL for an RR
593  @type last_ttl: int
594  @ivar last_ttl_known: Has last TTL been detected
595  @type last_ttl_known: bool
596  @ivar default_ttl: The default TTL from a $TTL directive or SOA RR
597  @type default_ttl: int
598  @ivar default_ttl_known: Has default TTL been detected
599  @type default_ttl_known: bool
600  @ivar last_name: The last name read
601  @type last_name: dns.name.Name object
602  @ivar current_origin: The current origin
603  @type current_origin: dns.name.Name object
604  @ivar relativize: should names in the zone be relativized?
605  @type relativize: bool
606  @ivar zone: the zone
607  @type zone: dns.zone.Zone object
608  @ivar saved_state: saved reader state (used when processing $INCLUDE)
609  @type saved_state: list of (tokenizer, current_origin, last_name, file,
610  last_ttl, last_ttl_known, default_ttl, default_ttl_known) tuples.
611  @ivar current_file: the file object of the $INCLUDed file being parsed
612  (None if no $INCLUDE is active).
613  @ivar allow_include: is $INCLUDE allowed?
614  @type allow_include: bool
615  @ivar check_origin: should sanity checks of the origin node be done?
616  The default is True.
617  @type check_origin: bool
618  """
619 
620  def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone,
621  allow_include=False, check_origin=True):
622  if isinstance(origin, string_types):
623  origin = dns.name.from_text(origin)
624  self.tok = tok
625  self.current_origin = origin
626  self.relativize = relativize
627  self.last_ttl = 0
628  self.last_ttl_known = False
629  self.default_ttl = 0
630  self.default_ttl_known = False
632  self.zone = zone_factory(origin, rdclass, relativize=relativize)
633  self.saved_state = []
634  self.current_file = None
635  self.allow_include = allow_include
636  self.check_origin = check_origin
637 
638  def _eat_line(self):
639  while 1:
640  token = self.tok.get()
641  if token.is_eol_or_eof():
642  break
643 
644  def _rr_line(self):
645  """Process one line from a DNS master file."""
646  # Name
647  if self.current_origin is None:
648  raise UnknownOrigin
649  token = self.tok.get(want_leading=True)
650  if not token.is_whitespace():
652  token.value, self.current_origin)
653  else:
654  token = self.tok.get()
655  if token.is_eol_or_eof():
656  # treat leading WS followed by EOL/EOF as if they were EOL/EOF.
657  return
658  self.tok.unget(token)
659  name = self.last_name
660  if not name.is_subdomain(self.zone.origin):
661  self._eat_line()
662  return
663  if self.relativize:
664  name = name.relativize(self.zone.origin)
665  token = self.tok.get()
666  if not token.is_identifier():
668  # TTL
669  try:
670  ttl = dns.ttl.from_text(token.value)
671  self.last_ttl = ttl
672  self.last_ttl_known = True
673  token = self.tok.get()
674  if not token.is_identifier():
676  except dns.ttl.BadTTL:
677  if not (self.last_ttl_known or self.default_ttl_known):
678  raise dns.exception.SyntaxError("Missing default TTL value")
679  if self.default_ttl_known:
680  ttl = self.default_ttl
681  else:
682  ttl = self.last_ttl
683  # Class
684  try:
685  rdclass = dns.rdataclass.from_text(token.value)
686  token = self.tok.get()
687  if not token.is_identifier():
691  except Exception:
692  rdclass = self.zone.rdclass
693  if rdclass != self.zone.rdclass:
694  raise dns.exception.SyntaxError("RR class is not zone's class")
695  # Type
696  try:
697  rdtype = dns.rdatatype.from_text(token.value)
698  except:
700  "unknown rdatatype '%s'" % token.value)
701  n = self.zone.nodes.get(name)
702  if n is None:
703  n = self.zone.node_factory()
704  self.zone.nodes[name] = n
705  try:
706  rd = dns.rdata.from_text(rdclass, rdtype, self.tok,
707  self.current_origin, False)
709  # Catch and reraise.
710  (ty, va) = sys.exc_info()[:2]
711  raise va
712  except:
713  # All exceptions that occur in the processing of rdata
714  # are treated as syntax errors. This is not strictly
715  # correct, but it is correct almost all of the time.
716  # We convert them to syntax errors so that we can emit
717  # helpful filename:line info.
718  (ty, va) = sys.exc_info()[:2]
720  "caught exception {}: {}".format(str(ty), str(va)))
721 
722  if not self.default_ttl_known and isinstance(rd, dns.rdtypes.ANY.SOA.SOA):
723  # The pre-RFC2308 and pre-BIND9 behavior inherits the zone default
724  # TTL from the SOA minttl if no $TTL statement is present before the
725  # SOA is parsed.
726  self.default_ttl = rd.minimum
727  self.default_ttl_known = True
728 
729  rd.choose_relativity(self.zone.origin, self.relativize)
730  covers = rd.covers()
731  rds = n.find_rdataset(rdclass, rdtype, covers, True)
732  rds.add(rd, ttl)
733 
734  def _parse_modify(self, side):
735  # Here we catch everything in '{' '}' in a group so we can replace it
736  # with ''.
737  is_generate1 = re.compile("^.*\$({(\+|-?)(\d+),(\d+),(.)}).*$")
738  is_generate2 = re.compile("^.*\$({(\+|-?)(\d+)}).*$")
739  is_generate3 = re.compile("^.*\$({(\+|-?)(\d+),(\d+)}).*$")
740  # Sometimes there are modifiers in the hostname. These come after
741  # the dollar sign. They are in the form: ${offset[,width[,base]]}.
742  # Make names
743  g1 = is_generate1.match(side)
744  if g1:
745  mod, sign, offset, width, base = g1.groups()
746  if sign == '':
747  sign = '+'
748  g2 = is_generate2.match(side)
749  if g2:
750  mod, sign, offset = g2.groups()
751  if sign == '':
752  sign = '+'
753  width = 0
754  base = 'd'
755  g3 = is_generate3.match(side)
756  if g3:
757  mod, sign, offset, width = g1.groups()
758  if sign == '':
759  sign = '+'
760  width = g1.groups()[2]
761  base = 'd'
762 
763  if not (g1 or g2 or g3):
764  mod = ''
765  sign = '+'
766  offset = 0
767  width = 0
768  base = 'd'
769 
770  if base != 'd':
771  raise NotImplementedError()
772 
773  return mod, sign, offset, width, base
774 
775  def _generate_line(self):
776  # range lhs [ttl] [class] type rhs [ comment ]
777  """Process one line containing the GENERATE statement from a DNS
778  master file."""
779  if self.current_origin is None:
780  raise UnknownOrigin
781 
782  token = self.tok.get()
783  # Range (required)
784  try:
785  start, stop, step = dns.grange.from_text(token.value)
786  token = self.tok.get()
787  if not token.is_identifier():
789  except:
791 
792  # lhs (required)
793  try:
794  lhs = token.value
795  token = self.tok.get()
796  if not token.is_identifier():
798  except:
800 
801  # TTL
802  try:
803  ttl = dns.ttl.from_text(token.value)
804  self.last_ttl = ttl
805  self.last_ttl_known = True
806  token = self.tok.get()
807  if not token.is_identifier():
809  except dns.ttl.BadTTL:
810  if not (self.last_ttl_known or self.default_ttl_known):
811  raise dns.exception.SyntaxError("Missing default TTL value")
812  if self.default_ttl_known:
813  ttl = self.default_ttl
814  else:
815  ttl = self.last_ttl
816  # Class
817  try:
818  rdclass = dns.rdataclass.from_text(token.value)
819  token = self.tok.get()
820  if not token.is_identifier():
824  except Exception:
825  rdclass = self.zone.rdclass
826  if rdclass != self.zone.rdclass:
827  raise dns.exception.SyntaxError("RR class is not zone's class")
828  # Type
829  try:
830  rdtype = dns.rdatatype.from_text(token.value)
831  token = self.tok.get()
832  if not token.is_identifier():
834  except Exception:
835  raise dns.exception.SyntaxError("unknown rdatatype '%s'" %
836  token.value)
837 
838  # lhs (required)
839  try:
840  rhs = token.value
841  except:
843 
844  lmod, lsign, loffset, lwidth, lbase = self._parse_modify(lhs)
845  rmod, rsign, roffset, rwidth, rbase = self._parse_modify(rhs)
846  for i in range(start, stop + 1, step):
847  # +1 because bind is inclusive and python is exclusive
848 
849  if lsign == u'+':
850  lindex = i + int(loffset)
851  elif lsign == u'-':
852  lindex = i - int(loffset)
853 
854  if rsign == u'-':
855  rindex = i - int(roffset)
856  elif rsign == u'+':
857  rindex = i + int(roffset)
858 
859  lzfindex = str(lindex).zfill(int(lwidth))
860  rzfindex = str(rindex).zfill(int(rwidth))
861 
862  name = lhs.replace(u'$%s' % (lmod), lzfindex)
863  rdata = rhs.replace(u'$%s' % (rmod), rzfindex)
864 
865  self.last_name = dns.name.from_text(name, self.current_origin)
866  name = self.last_name
867  if not name.is_subdomain(self.zone.origin):
868  self._eat_line()
869  return
870  if self.relativize:
871  name = name.relativize(self.zone.origin)
872 
873  n = self.zone.nodes.get(name)
874  if n is None:
875  n = self.zone.node_factory()
876  self.zone.nodes[name] = n
877  try:
878  rd = dns.rdata.from_text(rdclass, rdtype, rdata,
879  self.current_origin, False)
881  # Catch and reraise.
882  (ty, va) = sys.exc_info()[:2]
883  raise va
884  except:
885  # All exceptions that occur in the processing of rdata
886  # are treated as syntax errors. This is not strictly
887  # correct, but it is correct almost all of the time.
888  # We convert them to syntax errors so that we can emit
889  # helpful filename:line info.
890  (ty, va) = sys.exc_info()[:2]
891  raise dns.exception.SyntaxError("caught exception %s: %s" %
892  (str(ty), str(va)))
893 
894  rd.choose_relativity(self.zone.origin, self.relativize)
895  covers = rd.covers()
896  rds = n.find_rdataset(rdclass, rdtype, covers, True)
897  rds.add(rd, ttl)
898 
899  def read(self):
900  """Read a DNS master file and build a zone object.
901 
902  @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
903  @raises dns.zone.NoNS: No NS RRset was found at the zone origin
904  """
905 
906  try:
907  while 1:
908  token = self.tok.get(True, True)
909  if token.is_eof():
910  if self.current_file is not None:
911  self.current_file.close()
912  if len(self.saved_state) > 0:
913  (self.tok,
914  self.current_origin,
915  self.last_name,
916  self.current_file,
917  self.last_ttl,
918  self.last_ttl_known,
919  self.default_ttl,
920  self.default_ttl_known) = self.saved_state.pop(-1)
921  continue
922  break
923  elif token.is_eol():
924  continue
925  elif token.is_comment():
926  self.tok.get_eol()
927  continue
928  elif token.value[0] == u'$':
929  c = token.value.upper()
930  if c == u'$TTL':
931  token = self.tok.get()
932  if not token.is_identifier():
933  raise dns.exception.SyntaxError("bad $TTL")
934  self.default_ttl = dns.ttl.from_text(token.value)
935  self.default_ttl_known = True
936  self.tok.get_eol()
937  elif c == u'$ORIGIN':
938  self.current_origin = self.tok.get_name()
939  self.tok.get_eol()
940  if self.zone.origin is None:
941  self.zone.origin = self.current_origin
942  elif c == u'$INCLUDE' and self.allow_include:
943  token = self.tok.get()
944  filename = token.value
945  token = self.tok.get()
946  if token.is_identifier():
947  new_origin =\
948  dns.name.from_text(token.value,
949  self.current_origin)
950  self.tok.get_eol()
951  elif not token.is_eol_or_eof():
953  "bad origin in $INCLUDE")
954  else:
955  new_origin = self.current_origin
956  self.saved_state.append((self.tok,
957  self.current_origin,
958  self.last_name,
959  self.current_file,
960  self.last_ttl,
961  self.last_ttl_known,
962  self.default_ttl,
963  self.default_ttl_known))
964  self.current_file = open(filename, 'r')
966  filename)
967  self.current_origin = new_origin
968  elif c == u'$GENERATE':
969  self._generate_line()
970  else:
972  "Unknown master file directive '" + c + "'")
973  continue
974  self.tok.unget(token)
975  self._rr_line()
976  except dns.exception.SyntaxError as detail:
977  (filename, line_number) = self.tok.where()
978  if detail is None:
979  detail = "syntax error"
981  "%s:%d: %s" % (filename, line_number, detail))
982 
983  # Now that we're done reading, do some basic checking of the zone.
984  if self.check_origin:
985  self.zone.check_origin()
986 
987 
988 def from_text(text, origin=None, rdclass=dns.rdataclass.IN,
989  relativize=True, zone_factory=Zone, filename=None,
990  allow_include=False, check_origin=True):
991  """Build a zone object from a master file format string.
992 
993  @param text: the master file format input
994  @type text: string.
995  @param origin: The origin of the zone; if not specified, the first
996  $ORIGIN statement in the master file will determine the origin of the
997  zone.
998  @type origin: dns.name.Name object or string
999  @param rdclass: The zone's rdata class; the default is class IN.
1000  @type rdclass: int
1001  @param relativize: should names be relativized? The default is True
1002  @type relativize: bool
1003  @param zone_factory: The zone factory to use
1004  @type zone_factory: function returning a Zone
1005  @param filename: The filename to emit when describing where an error
1006  occurred; the default is '<string>'.
1007  @type filename: string
1008  @param allow_include: is $INCLUDE allowed?
1009  @type allow_include: bool
1010  @param check_origin: should sanity checks of the origin node be done?
1011  The default is True.
1012  @type check_origin: bool
1013  @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
1014  @raises dns.zone.NoNS: No NS RRset was found at the zone origin
1015  @rtype: dns.zone.Zone object
1016  """
1017 
1018  # 'text' can also be a file, but we don't publish that fact
1019  # since it's an implementation detail. The official file
1020  # interface is from_file().
1021 
1022  if filename is None:
1023  filename = '<string>'
1024  tok = dns.tokenizer.Tokenizer(text, filename)
1025  reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory,
1026  allow_include=allow_include,
1027  check_origin=check_origin)
1028  reader.read()
1029  return reader.zone
1030 
1031 
1032 def from_file(f, origin=None, rdclass=dns.rdataclass.IN,
1033  relativize=True, zone_factory=Zone, filename=None,
1034  allow_include=True, check_origin=True):
1035  """Read a master file and build a zone object.
1036 
1037  @param f: file or string. If I{f} is a string, it is treated
1038  as the name of a file to open.
1039  @param origin: The origin of the zone; if not specified, the first
1040  $ORIGIN statement in the master file will determine the origin of the
1041  zone.
1042  @type origin: dns.name.Name object or string
1043  @param rdclass: The zone's rdata class; the default is class IN.
1044  @type rdclass: int
1045  @param relativize: should names be relativized? The default is True
1046  @type relativize: bool
1047  @param zone_factory: The zone factory to use
1048  @type zone_factory: function returning a Zone
1049  @param filename: The filename to emit when describing where an error
1050  occurred; the default is '<file>', or the value of I{f} if I{f} is a
1051  string.
1052  @type filename: string
1053  @param allow_include: is $INCLUDE allowed?
1054  @type allow_include: bool
1055  @param check_origin: should sanity checks of the origin node be done?
1056  The default is True.
1057  @type check_origin: bool
1058  @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
1059  @raises dns.zone.NoNS: No NS RRset was found at the zone origin
1060  @rtype: dns.zone.Zone object
1061  """
1062 
1063  str_type = string_types
1064  if PY3:
1065  opts = 'r'
1066  else:
1067  opts = 'rU'
1068 
1069  if isinstance(f, str_type):
1070  if filename is None:
1071  filename = f
1072  f = open(f, opts)
1073  want_close = True
1074  else:
1075  if filename is None:
1076  filename = '<file>'
1077  want_close = False
1078 
1079  try:
1080  z = from_text(f, origin, rdclass, relativize, zone_factory,
1081  filename, allow_include, check_origin)
1082  finally:
1083  if want_close:
1084  f.close()
1085  return z
1086 
1087 
1088 def from_xfr(xfr, zone_factory=Zone, relativize=True, check_origin=True):
1089  """Convert the output of a zone transfer generator into a zone object.
1090 
1091  @param xfr: The xfr generator
1092  @type xfr: generator of dns.message.Message objects
1093  @param relativize: should names be relativized? The default is True.
1094  It is essential that the relativize setting matches the one specified
1095  to dns.query.xfr().
1096  @type relativize: bool
1097  @param check_origin: should sanity checks of the origin node be done?
1098  The default is True.
1099  @type check_origin: bool
1100  @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
1101  @raises dns.zone.NoNS: No NS RRset was found at the zone origin
1102  @rtype: dns.zone.Zone object
1103  """
1104 
1105  z = None
1106  for r in xfr:
1107  if z is None:
1108  if relativize:
1109  origin = r.origin
1110  else:
1111  origin = r.answer[0].name
1112  rdclass = r.answer[0].rdclass
1113  z = zone_factory(origin, rdclass, relativize=relativize)
1114  for rrset in r.answer:
1115  znode = z.nodes.get(rrset.name)
1116  if not znode:
1117  znode = z.node_factory()
1118  z.nodes[rrset.name] = znode
1119  zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype,
1120  rrset.covers, True)
1121  zrds.update_ttl(rrset.ttl)
1122  for rd in rrset:
1123  rd.choose_relativity(z.origin, relativize)
1124  zrds.add(rd)
1125  if check_origin:
1126  z.check_origin()
1127  return z
dns.zone.Zone.find_rdataset
def find_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE, create=False)
Definition: zone.py:239
dns.zone._MasterReader.check_origin
check_origin
Definition: zone.py:635
dns.zone.Zone.delete_rdataset
def delete_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE)
Definition: zone.py:308
dns.zone.NoSOA
Definition: zone.py:46
dns.zone.Zone.__contains__
def __contains__(self, other)
Definition: zone.py:186
dns.name.Name
Definition: name.py:318
dns.exception.SyntaxError
Definition: exception.py:113
dns.zone.Zone.get_node
def get_node(self, name, create=False)
Definition: zone.py:209
dns.zone.Zone.iterkeys
def iterkeys(self)
Definition: zone.py:159
dns.rdtypes.ANY.SOA
Definition: SOA.py:1
dns.rrset
Definition: rrset.py:1
dns.ttl.BadTTL
Definition: ttl.py:24
dns.name.from_text
def from_text(text, origin=root, idna_codec=None)
Definition: name.py:873
dns.zone.from_file
def from_file(f, origin=None, rdclass=dns.rdataclass.IN, relativize=True, zone_factory=Zone, filename=None, allow_include=True, check_origin=True)
Definition: zone.py:1032
dns.zone._MasterReader.allow_include
allow_include
Definition: zone.py:634
dns.zone.Zone.iterate_rdatasets
def iterate_rdatasets(self, rdtype=dns.rdatatype.ANY, covers=dns.rdatatype.NONE)
Definition: zone.py:437
dns.zone._MasterReader.last_name
last_name
Definition: zone.py:630
dns.zone._MasterReader
Definition: zone.py:586
dns.zone.Zone.values
def values(self)
Definition: zone.py:174
dns.rdataclass
Definition: rdataclass.py:1
dns.zone.Zone.get
def get(self, key)
Definition: zone.py:182
dns.zone.Zone.find_node
def find_node(self, name, create=False)
Definition: zone.py:189
dns.zone.Zone.to_text
def to_text(self, sorted=True, relativize=True, nl=None)
Definition: zone.py:548
dns.exception.DNSException
Definition: exception.py:24
dns.zone._MasterReader._eat_line
def _eat_line(self)
Definition: zone.py:638
dns.zone.from_text
def from_text(text, origin=None, rdclass=dns.rdataclass.IN, relativize=True, zone_factory=Zone, filename=None, allow_include=False, check_origin=True)
Definition: zone.py:988
dns.zone.Zone.itervalues
def itervalues(self)
Definition: zone.py:168
dns.zone.Zone.rdclass
rdclass
Definition: zone.py:106
dns.zone.Zone.origin
origin
Definition: zone.py:105
dns.zone.Zone.delete_node
def delete_node(self, name)
Definition: zone.py:229
dns.zone.Zone.iteritems
def iteritems
Definition: zone.py:180
dns.zone._MasterReader.last_ttl_known
last_ttl_known
Definition: zone.py:627
dns.ttl.from_text
def from_text(text)
Definition: ttl.py:28
dns.zone.Zone.node_factory
node_factory
Definition: zone.py:85
dns.zone._MasterReader.relativize
relativize
Definition: zone.py:625
dns.zone.Zone.iterate_rdatas
def iterate_rdatas(self, rdtype=dns.rdatatype.ANY, covers=dns.rdatatype.NONE)
Definition: zone.py:460
dns.zone.NoNS
Definition: zone.py:51
dns.zone.Zone.__delitem__
def __delitem__(self, key)
Definition: zone.py:152
dns.grange.from_text
def from_text(text)
Definition: grange.py:22
dns.zone.Zone.__setitem__
def __setitem__(self, key, value)
Definition: zone.py:148
dns.zone.Zone.check_origin
def check_origin(self)
Definition: zone.py:569
dns.rdata.from_text
def from_text(rdclass, rdtype, tok, origin=None, relativize=True)
Definition: rdata.py:344
dns.zone.Zone.__ne__
def __ne__(self, other)
Definition: zone.py:124
dns.node
Definition: node.py:1
dns.zone._MasterReader.current_file
current_file
Definition: zone.py:633
dns.zone.BadZone
Definition: zone.py:41
dns.zone._MasterReader._parse_modify
def _parse_modify(self, side)
Definition: zone.py:734
dns.hash.get
def get(algorithm)
Definition: hash.py:36
dns.zone.Zone.items
def items(self)
Definition: zone.py:177
dns.zone._MasterReader.tok
tok
Definition: zone.py:623
dns.zone._MasterReader._generate_line
def _generate_line(self)
Definition: zone.py:775
dns.zone._MasterReader.saved_state
saved_state
Definition: zone.py:632
dns.tokenizer.Tokenizer
Definition: tokenizer.py:152
dns.zone._MasterReader.last_ttl
last_ttl
Definition: zone.py:626
dns.name
Definition: name.py:1
dns.zone.Zone.get_rdataset
def get_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE, create=False)
Definition: zone.py:275
dns.zone._MasterReader.read
def read(self)
Definition: zone.py:899
dns.zone.Zone.get_rrset
def get_rrset(self, name, rdtype, covers=dns.rdatatype.NONE)
Definition: zone.py:403
dns.zone.Zone.replace_rdataset
def replace_rdataset(self, name, replacement)
Definition: zone.py:341
dns.zone.Zone.__getitem__
def __getitem__(self, key)
Definition: zone.py:144
dns.zone._MasterReader.__init__
def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone, allow_include=False, check_origin=True)
Definition: zone.py:620
dns.node.Node
Definition: node.py:27
dns.rdata
Definition: rdata.py:1
dns.rdatatype
Definition: rdatatype.py:1
dns.zone._MasterReader.default_ttl
default_ttl
Definition: zone.py:628
dns.zone.UnknownOrigin
Definition: zone.py:56
dns.ttl
Definition: ttl.py:1
dns.zone.Zone
Definition: zone.py:61
dns.rdatatype.from_text
def from_text(text)
Definition: rdatatype.py:193
dns.zone.Zone.nodes
nodes
Definition: zone.py:107
dns.zone.Zone.find_rrset
def find_rrset(self, name, rdtype, covers=dns.rdatatype.NONE)
Definition: zone.py:363
dns.zone._MasterReader.default_ttl_known
default_ttl_known
Definition: zone.py:629
dns.zone.from_xfr
def from_xfr(xfr, zone_factory=Zone, relativize=True, check_origin=True)
Definition: zone.py:1088
dns.zone._MasterReader.current_origin
current_origin
Definition: zone.py:624
dns.rrset.RRset
Definition: rrset.py:28
dns.zone.Zone._validate_name
def _validate_name(self, name)
Definition: zone.py:131
dns.zone.Zone.keys
def keys(self)
Definition: zone.py:165
dns.rdtypes.ANY.SOA.SOA
Definition: SOA.py:25
dns.zone._MasterReader.zone
zone
Definition: zone.py:631
dns.zone.Zone.to_file
def to_file(self, f, sorted=True, relativize=True, nl=None)
Definition: zone.py:484
dns.tokenizer
Definition: tokenizer.py:1
dns.exception
Definition: exception.py:1
dns.zone._MasterReader._rr_line
def _rr_line(self)
Definition: zone.py:644
dns.grange
Definition: grange.py:1
dns.rdataclass.from_text
def from_text(text)
Definition: rdataclass.py:67
dns.zone.Zone.__iter__
def __iter__(self)
Definition: zone.py:156
dns.zone.Zone.relativize
relativize
Definition: zone.py:108
dns.zone.Zone.__eq__
def __eq__(self, other)
Definition: zone.py:110
dns.zone.Zone.__init__
def __init__(self, origin, rdclass=dns.rdataclass.IN, relativize=True)
Definition: zone.py:89