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 18from __future__ import generators 19 20import sys 21 22import dns.exception 23import dns.name 24import dns.node 25import dns.rdataclass 26import dns.rdatatype 27import dns.rdata 28import dns.rrset 29import dns.tokenizer 30import dns.ttl 31 32class BadZone(dns.exception.DNSException): 33 """The zone is malformed.""" 34 pass 35 36class NoSOA(BadZone): 37 """The zone has no SOA RR at its origin.""" 38 pass 39 40class NoNS(BadZone): 41 """The zone has no NS RRset at its origin.""" 42 pass 43 44class UnknownOrigin(BadZone): 45 """The zone's origin is unknown.""" 46 pass 47 48class 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): 241 rdtype = dns.rdatatype.from_text(rdtype) 242 if isinstance(covers, str): 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): 304 rdtype = dns.rdatatype.from_text(rdtype) 305 if isinstance(covers, str): 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): 367 rdtype = dns.rdatatype.from_text(rdtype) 368 if isinstance(covers, str): 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): 423 rdtype = dns.rdatatype.from_text(rdtype) 424 if isinstance(covers, str): 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): 446 rdtype = dns.rdatatype.from_text(rdtype) 447 if isinstance(covers, str): 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 523class _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 721def 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 764def 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 820def 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