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

# Source Code for Module dns.zone

      1  # Copyright (C) 2003-2007, 2009, 2010 Nominum, Inc.
      2  #
      3  # Permission to use, copy, modify, and distribute this software and its
      4  # documentation for any purpose with or without fee is hereby granted,
      5  # provided that the above copyright notice and this permission notice
      6  # appear in all copies.
      7  #
      8  # THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
      9  # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     10  # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
     11  # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     12  # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     13  # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
     14  # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     15
     16  """DNS Zones."""
     17
     18  from __future__ import generators
     19
     20  import sys
     21
     22  import dns.exception
     23  import dns.name
     24  import dns.node
     25  import dns.rdataclass
     26  import dns.rdatatype
     27  import dns.rdata
     28  import dns.rrset
     29  import dns.tokenizer
     30  import dns.ttl
     31
     32 -class BadZone(dns.exception.DNSException):
     33      """The zone is malformed."""
     34      pass
     35
     36 -class NoSOA(BadZone):
     37      """The zone has no SOA RR at its origin."""
     38      pass
     39
     40 -class NoNS(BadZone):
     41      """The zone has no NS RRset at its origin."""
     42      pass
     43
     44 -class UnknownOrigin(BadZone):
     45      """The zone's origin is unknown."""
     46      pass
     47
     48 -class Zone(object):
     49      """A DNS zone.
     50
     51      A Zone is a mapping from names to nodes.  The zone object may be
     52      treated like a Python dictionary, e.g. zone[name] will retrieve
     53      the node associated with that name.  The I{name} may be a
     54      dns.name.Name object, or it may be a string.  In the either case,
     55      if the name is relative it is treated as relative to the origin of
     56      the zone.
     57
     58      @ivar rdclass: The zone's rdata class; the default is class IN.
     59      @type rdclass: int
     60      @ivar origin: The origin of the zone.
     61      @type origin: dns.name.Name object
     62      @ivar nodes: A dictionary mapping the names of nodes in the zone to the
     63      nodes themselves.
     64      @type nodes: dict
     65      @ivar relativize: should names in the zone be relativized?
     66      @type relativize: bool
     67      @cvar node_factory: the factory used to create a new node
     68      @type node_factory: class or callable
     69      """
     70
     71      node_factory = dns.node.Node
     72
     73      __slots__ = ['rdclass', 'origin', 'nodes', 'relativize']
     74
     75 -    def __init__(self, origin, rdclass=dns.rdataclass.IN, relativize=True):
     76          """Initialize a zone object.
     77
     78          @param origin: The origin of the zone.
     79          @type origin: dns.name.Name object
     80          @param rdclass: The zone's rdata class; the default is class IN.
     81          @type rdclass: int"""
     82
     83          self.rdclass = rdclass
     84          self.origin = origin
     85          self.nodes = {}
     86          self.relativize = relativize
     87
     88 -    def __eq__(self, other):
     89          """Two zones are equal if they have the same origin, class, and
     90          nodes.
     91          @rtype: bool
     92          """
     93
     94          if not isinstance(other, Zone):
     95              return False
     96          if self.rdclass != other.rdclass or \
     97             self.origin != other.origin or \
     98             self.nodes != other.nodes:
     99              return False
    100          return True
    101
    102 -    def __ne__(self, other):
    103          """Are two zones not equal?
    104          @rtype: bool
    105          """
    106
    107          return not self.__eq__(other)
    108
    109 -    def _validate_name(self, name):
    110          if isinstance(name, (str, unicode)):
    111              name = dns.name.from_text(name, None)
    112          elif not isinstance(name, dns.name.Name):
    113              raise KeyError("name parameter must be convertable to a DNS name")
    114          if name.is_absolute():
    115              if not name.is_subdomain(self.origin):
    116                  raise KeyError("name parameter must be a subdomain of the zone origin")
    117              if self.relativize:
    118                  name = name.relativize(self.origin)
    119          return name
    120
    121 -    def __getitem__(self, key):
    122          key = self._validate_name(key)
    123          return self.nodes[key]
    124
    125 -    def __setitem__(self, key, value):
    126          key = self._validate_name(key)
    127          self.nodes[key] = value
    128
    129 -    def __delitem__(self, key):
    130          key = self._validate_name(key)
    131          del self.nodes[key]
    132
    133 -    def __iter__(self):
    134          return self.nodes.iterkeys()
    135
    136 -    def iterkeys(self):
    137          return self.nodes.iterkeys()
    138
    139 -    def keys(self):
    140          return self.nodes.keys()
    141
    142 -    def itervalues(self):
    143          return self.nodes.itervalues()
    144
    145 -    def values(self):
    146          return self.nodes.values()
    147
    148 -    def iteritems(self):
    149          return self.nodes.iteritems()
    150
    151 -    def items(self):
    152          return self.nodes.items()
    153
    154 -    def get(self, key):
    155          key = self._validate_name(key)
    156          return self.nodes.get(key)
    157
    158 -    def __contains__(self, other):
    159          return other in self.nodes
    160
    161 -    def find_node(self, name, create=False):
    162          """Find a node in the zone, possibly creating it.
    163
    164          @param name: the name of the node to find
    165          @type name: dns.name.Name object or string
    166          @param create: should the node be created if it doesn't exist?
    167          @type create: bool
    168          @raises KeyError: the name is not known and create was not specified.
    169          @rtype: dns.node.Node object
    170          """
    171
    172          name = self._validate_name(name)
    173          node = self.nodes.get(name)
    174          if node is None:
    175              if not create:
    176                  raise KeyError
    177              node = self.node_factory()
    178              self.nodes[name] = node
    179          return node
    180
    181 -    def get_node(self, name, create=False):
    182          """Get a node in the zone, possibly creating it.
    183
    184          This method is like L{find_node}, except it returns None instead
    185          of raising an exception if the node does not exist and creation
    186          has not been requested.
    187
    188          @param name: the name of the node to find
    189          @type name: dns.name.Name object or string
    190          @param create: should the node be created if it doesn't exist?
    191          @type create: bool
    192          @rtype: dns.node.Node object or None
    193          """
    194
    195          try:
    196              node = self.find_node(name, create)
    197          except KeyError:
    198              node = None
    199          return node
    200
    201 -    def delete_node(self, name):
    202          """Delete the specified node if it exists.
    203
    204          It is not an error if the node does not exist.
    205          """
    206
    207          name = self._validate_name(name)
    208          if self.nodes.has_key(name):
    209              del self.nodes[name]
    210
    211 -    def find_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,
    212                        create=False):
    213          """Look for rdata with the specified name and type in the zone,
    214          and return an rdataset encapsulating it.
    215
    216          The I{name}, I{rdtype}, and I{covers} parameters may be
    217          strings, in which case they will be converted to their proper
    218          type.
    219
    220          The rdataset returned is not a copy; changes to it will change
    221          the zone.
    222
    223          KeyError is raised if the name or type are not found.
    224          Use L{get_rdataset} if you want to have None returned instead.
    225
    226          @param name: the owner name to look for
    227          @type name: DNS.name.Name object or string
    228          @param rdtype: the rdata type desired
    229          @type rdtype: int or string
    230          @param covers: the covered type (defaults to None)
    231          @type covers: int or string
    232          @param create: should the node and rdataset be created if they do not
    233          exist?
    234          @type create: bool
    235          @raises KeyError: the node or rdata could not be found
    236          @rtype: dns.rrset.RRset object
    237          """
    238
    239          name = self._validate_name(name)
    240          if isinstance(rdtype, (str, unicode)):
    241              rdtype = dns.rdatatype.from_text(rdtype)
    242          if isinstance(covers, (str, unicode)):
    243              covers = dns.rdatatype.from_text(covers)
    244          node = self.find_node(name, create)
    245          return node.find_rdataset(self.rdclass, rdtype, covers, create)
    246
    247 -    def get_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE,
    248                       create=False):
    249          """Look for rdata with the specified name and type in the zone,
    250          and return an rdataset encapsulating it.
    251
    252          The I{name}, I{rdtype}, and I{covers} parameters may be
    253          strings, in which case they will be converted to their proper
    254          type.
    255
    256          The rdataset returned is not a copy; changes to it will change
    257          the zone.
    258
    259          None is returned if the name or type are not found.
    260          Use L{find_rdataset} if you want to have KeyError raised instead.
    261
    262          @param name: the owner name to look for
    263          @type name: DNS.name.Name object or string
    264          @param rdtype: the rdata type desired
    265          @type rdtype: int or string
    266          @param covers: the covered type (defaults to None)
    267          @type covers: int or string
    268          @param create: should the node and rdataset be created if they do not
    269          exist?
    270          @type create: bool
    271          @rtype: dns.rrset.RRset object
    272          """
    273
    274          try:
    275              rdataset = self.find_rdataset(name, rdtype, covers, create)
    276          except KeyError:
    277              rdataset = None
    278          return rdataset
    279
    280 -    def delete_rdataset(self, name, rdtype, covers=dns.rdatatype.NONE):
    281          """Delete the rdataset matching I{rdtype} and I{covers}, if it
    282          exists at the node specified by I{name}.
    283
    284          The I{name}, I{rdtype}, and I{covers} parameters may be
    285          strings, in which case they will be converted to their proper
    286          type.
    287
    288          It is not an error if the node does not exist, or if there is no
    289          matching rdataset at the node.
    290
    291          If the node has no rdatasets after the deletion, it will itself
    292          be deleted.
    293
    294          @param name: the owner name to look for
    295          @type name: DNS.name.Name object or string
    296          @param rdtype: the rdata type desired
    297          @type rdtype: int or string
    298          @param covers: the covered type (defaults to None)
    299          @type covers: int or string
    300          """
    301
    302          name = self._validate_name(name)
    303          if isinstance(rdtype, (str, unicode)):
    304              rdtype = dns.rdatatype.from_text(rdtype)
    305          if isinstance(covers, (str, unicode)):
    306              covers = dns.rdatatype.from_text(covers)
    307          node = self.get_node(name)
    308          if not node is None:
    309              node.delete_rdataset(self.rdclass, rdtype, covers)
    310              if len(node) == 0:
    311                  self.delete_node(name)
    312
    313 -    def replace_rdataset(self, name, replacement):
    314          """Replace an rdataset at name.
    315
    316          It is not an error if there is no rdataset matching I{replacement}.
    317
    318          Ownership of the I{replacement} object is transferred to the zone;
    319          in other words, this method does not store a copy of I{replacement}
    320          at the node, it stores I{replacement} itself.
    321
    322          If the I{name} node does not exist, it is created.
    323
    324          @param name: the owner name
    325          @type name: DNS.name.Name object or string
    326          @param replacement: the replacement rdataset
    327          @type replacement: dns.rdataset.Rdataset
    328          """
    329
    330          if replacement.rdclass != self.rdclass:
    331              raise ValueError('replacement.rdclass != zone.rdclass')
    332          node = self.find_node(name, True)
    333          node.replace_rdataset(replacement)
    334
    335 -    def find_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):
    336          """Look for rdata with the specified name and type in the zone,
    337          and return an RRset encapsulating it.
    338
    339          The I{name}, I{rdtype}, and I{covers} parameters may be
    340          strings, in which case they will be converted to their proper
    341          type.
    342
    343          This method is less efficient than the similar
    344          L{find_rdataset} because it creates an RRset instead of
    345          returning the matching rdataset.  It may be more convenient
    346          for some uses since it returns an object which binds the owner
    347          name to the rdata.
    348
    349          This method may not be used to create new nodes or rdatasets;
    350          use L{find_rdataset} instead.
    351
    352          KeyError is raised if the name or type are not found.
    353          Use L{get_rrset} if you want to have None returned instead.
    354
    355          @param name: the owner name to look for
    356          @type name: DNS.name.Name object or string
    357          @param rdtype: the rdata type desired
    358          @type rdtype: int or string
    359          @param covers: the covered type (defaults to None)
    360          @type covers: int or string
    361          @raises KeyError: the node or rdata could not be found
    362          @rtype: dns.rrset.RRset object
    363          """
    364
    365          name = self._validate_name(name)
    366          if isinstance(rdtype, (str, unicode)):
    367              rdtype = dns.rdatatype.from_text(rdtype)
    368          if isinstance(covers, (str, unicode)):
    369              covers = dns.rdatatype.from_text(covers)
    370          rdataset = self.nodes[name].find_rdataset(self.rdclass, rdtype, covers)
    371          rrset = dns.rrset.RRset(name, self.rdclass, rdtype, covers)
    372          rrset.update(rdataset)
    373          return rrset
    374
    375 -    def get_rrset(self, name, rdtype, covers=dns.rdatatype.NONE):
    376          """Look for rdata with the specified name and type in the zone,
    377          and return an RRset encapsulating it.
    378
    379          The I{name}, I{rdtype}, and I{covers} parameters may be
    380          strings, in which case they will be converted to their proper
    381          type.
    382
    383          This method is less efficient than the similar L{get_rdataset}
    384          because it creates an RRset instead of returning the matching
    385          rdataset.  It may be more convenient for some uses since it
    386          returns an object which binds the owner name to the rdata.
    387
    388          This method may not be used to create new nodes or rdatasets;
    389          use L{find_rdataset} instead.
    390
    391          None is returned if the name or type are not found.
    392          Use L{find_rrset} if you want to have KeyError raised instead.
    393
    394          @param name: the owner name to look for
    395          @type name: DNS.name.Name object or string
    396          @param rdtype: the rdata type desired
    397          @type rdtype: int or string
    398          @param covers: the covered type (defaults to None)
    399          @type covers: int or string
    400          @rtype: dns.rrset.RRset object
    401          """
    402
    403          try:
    404              rrset = self.find_rrset(name, rdtype, covers)
    405          except KeyError:
    406              rrset = None
    407          return rrset
    408
    409 -    def iterate_rdatasets(self, rdtype=dns.rdatatype.ANY,
    410                            covers=dns.rdatatype.NONE):
    411          """Return a generator which yields (name, rdataset) tuples for
    412          all rdatasets in the zone which have the specified I{rdtype}
    413          and I{covers}.  If I{rdtype} is dns.rdatatype.ANY, the default,
    414          then all rdatasets will be matched.
    415
    416          @param rdtype: int or string
    417          @type rdtype: int or string
    418          @param covers: the covered type (defaults to None)
    419          @type covers: int or string
    420          """
    421
    422          if isinstance(rdtype, (str, unicode)):
    423              rdtype = dns.rdatatype.from_text(rdtype)
    424          if isinstance(covers, (str, unicode)):
    425              covers = dns.rdatatype.from_text(covers)
    426          for (name, node) in self.iteritems():
    427              for rds in node:
    428                  if rdtype == dns.rdatatype.ANY or \
    429                     (rds.rdtype == rdtype and rds.covers == covers):
    430                      yield (name, rds)
    431
    432 -    def iterate_rdatas(self, rdtype=dns.rdatatype.ANY,
    433                         covers=dns.rdatatype.NONE):
    434          """Return a generator which yields (name, ttl, rdata) tuples for
    435          all rdatas in the zone which have the specified I{rdtype}
    436          and I{covers}.  If I{rdtype} is dns.rdatatype.ANY, the default,
    437          then all rdatas will be matched.
    438
    439          @param rdtype: int or string
    440          @type rdtype: int or string
    441          @param covers: the covered type (defaults to None)
    442          @type covers: int or string
    443          """
    444
    445          if isinstance(rdtype, (str, unicode)):
    446              rdtype = dns.rdatatype.from_text(rdtype)
    447          if isinstance(covers, (str, unicode)):
    448              covers = dns.rdatatype.from_text(covers)
    449          for (name, node) in self.iteritems():
    450              for rds in node:
    451                  if rdtype == dns.rdatatype.ANY or \
    452                     (rds.rdtype == rdtype and rds.covers == covers):
    453                      for rdata in rds:
    454                          yield (name, rds.ttl, rdata)
    455
    456 -    def to_file(self, f, sorted=True, relativize=True, nl=None):
    457          """Write a zone to a file.
    458
    459          @param f: file or string.  If I{f} is a string, it is treated
    460          as the name of a file to open.
    461          @param sorted: if True, the file will be written with the
    462          names sorted in DNSSEC order from least to greatest.  Otherwise
    463          the names will be written in whatever order they happen to have
    464          in the zone's dictionary.
    465          @param relativize: if True, domain names in the output will be
    466          relativized to the zone's origin (if possible).
    467          @type relativize: bool
    468          @param nl: The end of line string.  If not specified, the
    469          output will use the platform's native end-of-line marker (i.e.
    470          LF on POSIX, CRLF on Windows, CR on Macintosh).
    471          @type nl: string or None
    472          """
    473
    474          if sys.hexversion >= 0x02030000:
    475              # allow Unicode filenames
    476              str_type = basestring
    477          else:
    478              str_type = str
    479          if nl is None:
    480              opts = 'w'
    481          else:
    482              opts = 'wb'
    483          if isinstance(f, str_type):
    484              f = file(f, opts)
    485              want_close = True
    486          else:
    487              want_close = False
    488          try:
    489              if sorted:
    490                  names = self.keys()
    491                  names.sort()
    492              else:
    493                  names = self.iterkeys()
    494              for n in names:
    495                  l = self[n].to_text(n, origin=self.origin,
    496                                      relativize=relativize)
    497                  if nl is None:
    498                      print >> f, l
    499                  else:
    500                      f.write(l)
    501                      f.write(nl)
    502          finally:
    503              if want_close:
    504                  f.close()
    505
    506 -    def check_origin(self):
    507          """Do some simple checking of the zone's origin.
    508
    509          @raises dns.zone.NoSOA: there is no SOA RR
    510          @raises dns.zone.NoNS: there is no NS RRset
    511          @raises KeyError: there is no origin node
    512          """
    513          if self.relativize:
    514              name = dns.name.empty
    515          else:
    516              name = self.origin
    517          if self.get_rdataset(name, dns.rdatatype.SOA) is None:
    518              raise NoSOA
    519          if self.get_rdataset(name, dns.rdatatype.NS) is None:
    520              raise NoNS
    521
    522
    523 -class _MasterReader(object):
    524      """Read a DNS master file
    525
    526      @ivar tok: The tokenizer
    527      @type tok: dns.tokenizer.Tokenizer object
    528      @ivar ttl: The default TTL
    529      @type ttl: int
    530      @ivar last_name: The last name read
    531      @type last_name: dns.name.Name object
    532      @ivar current_origin: The current origin
    533      @type current_origin: dns.name.Name object
    534      @ivar relativize: should names in the zone be relativized?
    535      @type relativize: bool
    536      @ivar zone: the zone
    537      @type zone: dns.zone.Zone object
    538      @ivar saved_state: saved reader state (used when processing $INCLUDE)
    539      @type saved_state: list of (tokenizer, current_origin, last_name, file)
    540      tuples.
    541      @ivar current_file: the file object of the $INCLUDed file being parsed
    542      (None if no $INCLUDE is active).
    543      @ivar allow_include: is $INCLUDE allowed?
    544      @type allow_include: bool
    545      @ivar check_origin: should sanity checks of the origin node be done?
    546      The default is True.
    547      @type check_origin: bool
    548      """
    549
    550 -    def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone,
    551                   allow_include=False, check_origin=True):
    552          if isinstance(origin, (str, unicode)):
    553              origin = dns.name.from_text(origin)
    554          self.tok = tok
    555          self.current_origin = origin
    556          self.relativize = relativize
    557          self.ttl = 0
    558          self.last_name = None
    559          self.zone = zone_factory(origin, rdclass, relativize=relativize)
    560          self.saved_state = []
    561          self.current_file = None
    562          self.allow_include = allow_include
    563          self.check_origin = check_origin
    564
    565 -    def _eat_line(self):
    566          while 1:
    567              token = self.tok.get()
    568              if token.is_eol_or_eof():
    569                  break
    570
    571 -    def _rr_line(self):
    572          """Process one line from a DNS master file."""
    573          # Name
    574          if self.current_origin is None:
    575              raise UnknownOrigin
    576          token = self.tok.get(want_leading = True)
    577          if not token.is_whitespace():
    578              self.last_name = dns.name.from_text(token.value, self.current_origin)
    579          else:
    580              token = self.tok.get()
    581              if token.is_eol_or_eof():
    582                  # treat leading WS followed by EOL/EOF as if they were EOL/EOF.
    583                  return
    584              self.tok.unget(token)
    585          name = self.last_name
    586          if not name.is_subdomain(self.zone.origin):
    587              self._eat_line()
    588              return
    589          if self.relativize:
    590              name = name.relativize(self.zone.origin)
    591          token = self.tok.get()
    592          if not token.is_identifier():
    593              raise dns.exception.SyntaxError
    594          # TTL
    595          try:
    596              ttl = dns.ttl.from_text(token.value)
    597              token = self.tok.get()
    598              if not token.is_identifier():
    599                  raise dns.exception.SyntaxError
    600          except dns.ttl.BadTTL:
    601              ttl = self.ttl
    602          # Class
    603          try:
    604              rdclass = dns.rdataclass.from_text(token.value)
    605              token = self.tok.get()
    606              if not token.is_identifier():
    607                  raise dns.exception.SyntaxError
    608          except dns.exception.SyntaxError:
    609              raise dns.exception.SyntaxError
    610          except:
    611              rdclass = self.zone.rdclass
    612          if rdclass != self.zone.rdclass:
    613              raise dns.exception.SyntaxError("RR class is not zone's class")
    614          # Type
    615          try:
    616              rdtype = dns.rdatatype.from_text(token.value)
    617          except:
    618              raise dns.exception.SyntaxError("unknown rdatatype '%s'" % token.value)
    619          n = self.zone.nodes.get(name)
    620          if n is None:
    621              n = self.zone.node_factory()
    622              self.zone.nodes[name] = n
    623          try:
    624              rd = dns.rdata.from_text(rdclass, rdtype, self.tok,
    625                                       self.current_origin, False)
    626          except dns.exception.SyntaxError:
    627              # Catch and reraise.
    628              (ty, va) = sys.exc_info()[:2]
    629              raise va
    630          except:
    631              # All exceptions that occur in the processing of rdata
    632              # are treated as syntax errors.  This is not strictly
    633              # correct, but it is correct almost all of the time.
    634              # We convert them to syntax errors so that we can emit
    635              # helpful filename:line info.
    636              (ty, va) = sys.exc_info()[:2]
    637              raise dns.exception.SyntaxError("caught exception %s: %s" % (str(ty), str(va)))
    638
    639          rd.choose_relativity(self.zone.origin, self.relativize)
    640          covers = rd.covers()
    641          rds = n.find_rdataset(rdclass, rdtype, covers, True)
    642          rds.add(rd, ttl)
    643
    644 -    def read(self):
    645          """Read a DNS master file and build a zone object.
    646
    647          @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
    648          @raises dns.zone.NoNS: No NS RRset was found at the zone origin
    649          """
    650
    651          try:
    652              while 1:
    653                  token = self.tok.get(True, True).unescape()
    654                  if token.is_eof():
    655                      if not self.current_file is None:
    656                          self.current_file.close()
    657                      if len(self.saved_state) > 0:
    658                          (self.tok,
    659                           self.current_origin,
    660                           self.last_name,
    661                           self.current_file,
    662                           self.ttl) = self.saved_state.pop(-1)
    663                          continue
    664                      break
    665                  elif token.is_eol():
    666                      continue
    667                  elif token.is_comment():
    668                      self.tok.get_eol()
    669                      continue
    670                  elif token.value[0] == '$':
    671                      u = token.value.upper()
    672                      if u == '$TTL':
    673                          token = self.tok.get()
    674                          if not token.is_identifier():
    675                              raise dns.exception.SyntaxError("bad $TTL")
    676                          self.ttl = dns.ttl.from_text(token.value)
    677                          self.tok.get_eol()
    678                      elif u == '$ORIGIN':
    679                          self.current_origin = self.tok.get_name()
    680                          self.tok.get_eol()
    681                          if self.zone.origin is None:
    682                              self.zone.origin = self.current_origin
    683                      elif u == '$INCLUDE' and self.allow_include:
    684                          token = self.tok.get()
    685                          if not token.is_quoted_string():
    686                              raise dns.exception.SyntaxError("bad filename in $INCLUDE")
    687                          filename = token.value
    688                          token = self.tok.get()
    689                          if token.is_identifier():
    690                              new_origin = dns.name.from_text(token.value, \
    691                                                              self.current_origin)
    692                              self.tok.get_eol()
    693                          elif not token.is_eol_or_eof():
    694                              raise dns.exception.SyntaxError("bad origin in $INCLUDE")
    695                          else:
    696                              new_origin = self.current_origin
    697                          self.saved_state.append((self.tok,
    698                                                   self.current_origin,
    699                                                   self.last_name,
    700                                                   self.current_file,
    701                                                   self.ttl))
    702                          self.current_file = file(filename, 'r')
    703                          self.tok = dns.tokenizer.Tokenizer(self.current_file,
    704                                                             filename)
    705                          self.current_origin = new_origin
    706                      else:
    707                          raise dns.exception.SyntaxError("Unknown master file directive '" + u + "'")
    708                      continue
    709                  self.tok.unget(token)
    710                  self._rr_line()
    711          except dns.exception.SyntaxError, detail:
    712              (filename, line_number) = self.tok.where()
    713              if detail is None:
    714                  detail = "syntax error"
    715              raise dns.exception.SyntaxError("%s:%d: %s" % (filename, line_number, detail))
    716
    717          # Now that we're done reading, do some basic checking of the zone.
    718          if self.check_origin:
    719              self.zone.check_origin()
    720
    721 -def from_text(text, origin = None, rdclass = dns.rdataclass.IN,
    722                relativize = True, zone_factory=Zone, filename=None,
    723                allow_include=False, check_origin=True):
    724      """Build a zone object from a master file format string.
    725
    726      @param text: the master file format input
    727      @type text: string.
    728      @param origin: The origin of the zone; if not specified, the first
    729      $ORIGIN statement in the master file will determine the origin of the
    730      zone.
    731      @type origin: dns.name.Name object or string
    732      @param rdclass: The zone's rdata class; the default is class IN.
    733      @type rdclass: int
    734      @param relativize: should names be relativized?  The default is True
    735      @type relativize: bool
    736      @param zone_factory: The zone factory to use
    737      @type zone_factory: function returning a Zone
    738      @param filename: The filename to emit when describing where an error
    739      occurred; the default is '<string>'.
    740      @type filename: string
    741      @param allow_include: is $INCLUDE allowed?
    742      @type allow_include: bool
    743      @param check_origin: should sanity checks of the origin node be done?
    744      The default is True.
    745      @type check_origin: bool
    746      @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
    747      @raises dns.zone.NoNS: No NS RRset was found at the zone origin
    748      @rtype: dns.zone.Zone object
    749      """
    750
    751      # 'text' can also be a file, but we don't publish that fact
    752      # since it's an implementation detail.  The official file
    753      # interface is from_file().
    754
    755      if filename is None:
    756          filename = '<string>'
    757      tok = dns.tokenizer.Tokenizer(text, filename)
    758      reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory,
    759                             allow_include=allow_include,
    760                             check_origin=check_origin)
    761      reader.read()
    762      return reader.zone
    763
    764 -def from_file(f, origin = None, rdclass = dns.rdataclass.IN,
    765                relativize = True, zone_factory=Zone, filename=None,
    766                allow_include=True, check_origin=True):
    767      """Read a master file and build a zone object.
    768
    769      @param f: file or string.  If I{f} is a string, it is treated
    770      as the name of a file to open.
    771      @param origin: The origin of the zone; if not specified, the first
    772      $ORIGIN statement in the master file will determine the origin of the
    773      zone.
    774      @type origin: dns.name.Name object or string
    775      @param rdclass: The zone's rdata class; the default is class IN.
    776      @type rdclass: int
    777      @param relativize: should names be relativized?  The default is True
    778      @type relativize: bool
    779      @param zone_factory: The zone factory to use
    780      @type zone_factory: function returning a Zone
    781      @param filename: The filename to emit when describing where an error
    782      occurred; the default is '<file>', or the value of I{f} if I{f} is a
    783      string.
    784      @type filename: string
    785      @param allow_include: is $INCLUDE allowed?
    786      @type allow_include: bool
    787      @param check_origin: should sanity checks of the origin node be done?
    788      The default is True.
    789      @type check_origin: bool
    790      @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
    791      @raises dns.zone.NoNS: No NS RRset was found at the zone origin
    792      @rtype: dns.zone.Zone object
    793      """
    794
    795      if sys.hexversion >= 0x02030000:
    796          # allow Unicode filenames; turn on universal newline support
    797          str_type = basestring
    798          opts = 'rU'
    799      else:
    800          str_type = str
    801          opts = 'r'
    802      if isinstance(f, str_type):
    803          if filename is None:
    804              filename = f
    805          f = file(f, opts)
    806          want_close = True
    807      else:
    808          if filename is None:
    809              filename = '<file>'
    810          want_close = False
    811
    812      try:
    813          z = from_text(f, origin, rdclass, relativize, zone_factory,
    814                        filename, allow_include, check_origin)
    815      finally:
    816          if want_close:
    817              f.close()
    818      return z
    819
    820 -def from_xfr(xfr, zone_factory=Zone, relativize=True):
    821      """Convert the output of a zone transfer generator into a zone object.
    822
    823      @param xfr: The xfr generator
    824      @type xfr: generator of dns.message.Message objects
    825      @param relativize: should names be relativized?  The default is True.
    826      It is essential that the relativize setting matches the one specified
    827      to dns.query.xfr().
    828      @type relativize: bool
    829      @raises dns.zone.NoSOA: No SOA RR was found at the zone origin
    830      @raises dns.zone.NoNS: No NS RRset was found at the zone origin
    831      @rtype: dns.zone.Zone object
    832      """
    833
    834      z = None
    835      for r in xfr:
    836          if z is None:
    837              if relativize:
    838                  origin = r.origin
    839              else:
    840                  origin = r.answer[0].name
    841              rdclass = r.answer[0].rdclass
    842              z = zone_factory(origin, rdclass, relativize=relativize)
    843          for rrset in r.answer:
    844              znode = z.nodes.get(rrset.name)
    845              if not znode:
    846                  znode = z.node_factory()
    847                  z.nodes[rrset.name] = znode
    848              zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype,
    849                                         rrset.covers, True)
    850              zrds.update_ttl(rrset.ttl)
    851              for rd in rrset:
    852                  rd.choose_relativity(z.origin, relativize)
    853                  zrds.add(rd)
    854      z.check_origin()
    855      return z
    856