Package dns :: Module zone |
[hide private]
[frames] | no frames]

# Source Code for Module dns.zone

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