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