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   
35 -class BadZone(dns.exception.DNSException):
36 """The zone is malformed.""" 37 pass
38
39 -class NoSOA(BadZone):
40 """The zone has no SOA RR at its origin.""" 41 pass
42
43 -class NoNS(BadZone):
44 """The zone has no NS RRset at its origin.""" 45 pass
46
47 -class UnknownOrigin(BadZone):
48 """The zone's origin is unknown.""" 49 pass
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 check_origin(self):
510 """Do some simple checking of the zone's origin. 511 512 @raises dns.zone.NoSOA: there is no SOA RR 513 @raises dns.zone.NoNS: there is no NS RRset 514 @raises KeyError: there is no origin node 515 """ 516 if self.relativize: 517 name = dns.name.empty 518 else: 519 name = self.origin 520 if self.get_rdataset(name, dns.rdatatype.SOA) is None: 521 raise NoSOA 522 if self.get_rdataset(name, dns.rdatatype.NS) is None: 523 raise NoNS
524 525
526 -class _MasterReader(object):
527 """Read a DNS master file 528 529 @ivar tok: The tokenizer 530 @type tok: dns.tokenizer.Tokenizer object 531 @ivar ttl: The default TTL 532 @type ttl: int 533 @ivar last_name: The last name read 534 @type last_name: dns.name.Name object 535 @ivar current_origin: The current origin 536 @type current_origin: dns.name.Name object 537 @ivar relativize: should names in the zone be relativized? 538 @type relativize: bool 539 @ivar zone: the zone 540 @type zone: dns.zone.Zone object 541 @ivar saved_state: saved reader state (used when processing $INCLUDE) 542 @type saved_state: list of (tokenizer, current_origin, last_name, file) 543 tuples. 544 @ivar current_file: the file object of the $INCLUDed file being parsed 545 (None if no $INCLUDE is active). 546 @ivar allow_include: is $INCLUDE allowed? 547 @type allow_include: bool 548 @ivar check_origin: should sanity checks of the origin node be done? 549 The default is True. 550 @type check_origin: bool 551 """ 552
553 - def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone, 554 allow_include=False, check_origin=True):
555 if isinstance(origin, (str, unicode)): 556 origin = dns.name.from_text(origin) 557 self.tok = tok 558 self.current_origin = origin 559 self.relativize = relativize 560 self.ttl = 0 561 self.last_name = None 562 self.zone = zone_factory(origin, rdclass, relativize=relativize) 563 self.saved_state = [] 564 self.current_file = None 565 self.allow_include = allow_include 566 self.check_origin = check_origin
567
568 - def _eat_line(self):
569 while 1: 570 token = self.tok.get() 571 if token.is_eol_or_eof(): 572 break
573
574 - def _rr_line(self):
575 """Process one line from a DNS master file.""" 576 # Name 577 if self.current_origin is None: 578 raise UnknownOrigin 579 token = self.tok.get(want_leading = True) 580 if not token.is_whitespace(): 581 self.last_name = dns.name.from_text(token.value, self.current_origin) 582 else: 583 token = self.tok.get() 584 if token.is_eol_or_eof(): 585 # treat leading WS followed by EOL/EOF as if they were EOL/EOF. 586 return 587 self.tok.unget(token) 588 name = self.last_name 589 if not name.is_subdomain(self.zone.origin): 590 self._eat_line() 591 return 592 if self.relativize: 593 name = name.relativize(self.zone.origin) 594 token = self.tok.get() 595 if not token.is_identifier(): 596 raise dns.exception.SyntaxError 597 # TTL 598 try: 599 ttl = dns.ttl.from_text(token.value) 600 token = self.tok.get() 601 if not token.is_identifier(): 602 raise dns.exception.SyntaxError 603 except dns.ttl.BadTTL: 604 ttl = self.ttl 605 # Class 606 try: 607 rdclass = dns.rdataclass.from_text(token.value) 608 token = self.tok.get() 609 if not token.is_identifier(): 610 raise dns.exception.SyntaxError 611 except dns.exception.SyntaxError: 612 raise dns.exception.SyntaxError 613 except: 614 rdclass = self.zone.rdclass 615 if rdclass != self.zone.rdclass: 616 raise dns.exception.SyntaxError("RR class is not zone's class") 617 # Type 618 try: 619 rdtype = dns.rdatatype.from_text(token.value) 620 except: 621 raise dns.exception.SyntaxError("unknown rdatatype '%s'" % token.value) 622 n = self.zone.nodes.get(name) 623 if n is None: 624 n = self.zone.node_factory() 625 self.zone.nodes[name] = n 626 try: 627 rd = dns.rdata.from_text(rdclass, rdtype, self.tok, 628 self.current_origin, False) 629 except dns.exception.SyntaxError: 630 # Catch and reraise. 631 (ty, va) = sys.exc_info()[:2] 632 raise va 633 except: 634 # All exceptions that occur in the processing of rdata 635 # are treated as syntax errors. This is not strictly 636 # correct, but it is correct almost all of the time. 637 # We convert them to syntax errors so that we can emit 638 # helpful filename:line info. 639 (ty, va) = sys.exc_info()[:2] 640 raise dns.exception.SyntaxError("caught exception %s: %s" % (str(ty), str(va))) 641 642 rd.choose_relativity(self.zone.origin, self.relativize) 643 covers = rd.covers() 644 rds = n.find_rdataset(rdclass, rdtype, covers, True) 645 rds.add(rd, ttl)
646
647 - def _parse_modify(self, side):
648 # Here we catch everything in '{' '}' in a group so we can replace it 649 # with ''. 650 is_generate1 = re.compile("^.*\$({(\+|-?)(\d+),(\d+),(.)}).*$") 651 is_generate2 = re.compile("^.*\$({(\+|-?)(\d+)}).*$") 652 is_generate3 = re.compile("^.*\$({(\+|-?)(\d+),(\d+)}).*$") 653 # Sometimes there are modifiers in the hostname. These come after 654 # the dollar sign. They are in the form: ${offset[,width[,base]]}. 655 # Make names 656 g1 = is_generate1.match(side) 657 if g1: 658 mod, sign, offset, width, base = g1.groups() 659 if sign == '': 660 sign = '+' 661 g2 = is_generate2.match(side) 662 if g2: 663 mod, sign, offset = g2.groups() 664 if sign == '': 665 sign = '+' 666 width = 0 667 base = 'd' 668 g3 = is_generate3.match(side) 669 if g3: 670 mod, sign, offset, width = g1.groups() 671 if sign == '': 672 sign = '+' 673 width = g1.groups()[2] 674 base = 'd' 675 676 if not (g1 or g2 or g3): 677 mod = '' 678 sign = '+' 679 offset = 0 680 width = 0 681 base = 'd' 682 683 if base != 'd': 684 raise NotImplemented 685 686 return mod, sign, offset, width, base
687
688 - def _generate_line(self):
689 # range lhs [ttl] [class] type rhs [ comment ] 690 """Process one line containing the GENERATE statement from a DNS 691 master file.""" 692 if self.current_origin is None: 693 raise UnknownOrigin 694 695 token = self.tok.get() 696 # Range (required) 697 try: 698 start, stop, step = dns.grange.from_text(token.value) 699 token = self.tok.get() 700 if not token.is_identifier(): 701 raise dns.exception.SyntaxError 702 except: 703 raise dns.exception.SyntaxError 704 705 # lhs (required) 706 try: 707 lhs = token.value 708 token = self.tok.get() 709 if not token.is_identifier(): 710 raise dns.exception.SyntaxError 711 except: 712 raise dns.exception.SyntaxError 713 714 # TTL 715 try: 716 ttl = dns.ttl.from_text(token.value) 717 token = self.tok.get() 718 if not token.is_identifier(): 719 raise dns.exception.SyntaxError 720 except dns.ttl.BadTTL: 721 ttl = self.ttl 722 # Class 723 try: 724 rdclass = dns.rdataclass.from_text(token.value) 725 token = self.tok.get() 726 if not token.is_identifier(): 727 raise dns.exception.SyntaxError 728 except dns.exception.SyntaxError: 729 raise dns.exception.SyntaxError 730 except: 731 rdclass = self.zone.rdclass 732 if rdclass != self.zone.rdclass: 733 raise dns.exception.SyntaxError("RR class is not zone's class") 734 # Type 735 try: 736 rdtype = dns.rdatatype.from_text(token.value) 737 token = self.tok.get() 738 if not token.is_identifier(): 739 raise dns.exception.SyntaxError 740 except: 741 raise dns.exception.SyntaxError("unknown rdatatype '%s'" % 742 token.value) 743 744 # lhs (required) 745 try: 746 rhs = token.value 747 except: 748 raise dns.exception.SyntaxError 749 750 751 lmod, lsign, loffset, lwidth, lbase = self._parse_modify(lhs) 752 rmod, rsign, roffset, rwidth, rbase = self._parse_modify(rhs) 753 for i in range(start, stop + 1, step): 754 # +1 because bind is inclusive and python is exclusive 755 756 if lsign == '+': 757 lindex = i + int(loffset) 758 elif lsign == '-': 759 lindex = i - int(loffset) 760 761 if rsign == '-': 762 rindex = i - int(roffset) 763 elif rsign == '+': 764 rindex = i + int(roffset) 765 766 lzfindex = str(lindex).zfill(int(lwidth)) 767 rzfindex = str(rindex).zfill(int(rwidth)) 768 769 770 name = lhs.replace('$%s' % (lmod), lzfindex) 771 rdata = rhs.replace('$%s' % (rmod), rzfindex) 772 773 self.last_name = dns.name.from_text(name, self.current_origin) 774 name = self.last_name 775 if not name.is_subdomain(self.zone.origin): 776 self._eat_line() 777 return 778 if self.relativize: 779 name = name.relativize(self.zone.origin) 780 781 n = self.zone.nodes.get(name) 782 if n is None: 783 n = self.zone.node_factory() 784 self.zone.nodes[name] = n 785 try: 786 rd = dns.rdata.from_text(rdclass, rdtype, rdata, 787 self.current_origin, False) 788 except dns.exception.SyntaxError: 789 # Catch and reraise. 790 (ty, va) = sys.exc_info()[:2] 791 raise va 792 except: 793 # All exceptions that occur in the processing of rdata 794 # are treated as syntax errors. This is not strictly 795 # correct, but it is correct almost all of the time. 796 # We convert them to syntax errors so that we can emit 797 # helpful filename:line info. 798 (ty, va) = sys.exc_info()[:2] 799 raise dns.exception.SyntaxError("caught exception %s: %s" % 800 (str(ty), str(va))) 801 802 rd.choose_relativity(self.zone.origin, self.relativize) 803 covers = rd.covers() 804 rds = n.find_rdataset(rdclass, rdtype, covers, True) 805 rds.add(rd, ttl)
806
807 - def read(self):
808 """Read a DNS master file and build a zone object. 809 810 @raises dns.zone.NoSOA: No SOA RR was found at the zone origin 811 @raises dns.zone.NoNS: No NS RRset was found at the zone origin 812 """ 813 814 try: 815 while 1: 816 token = self.tok.get(True, True).unescape() 817 if token.is_eof(): 818 if not self.current_file is None: 819 self.current_file.close() 820 if len(self.saved_state) > 0: 821 (self.tok, 822 self.current_origin, 823 self.last_name, 824 self.current_file, 825 self.ttl) = self.saved_state.pop(-1) 826 continue 827 break 828 elif token.is_eol(): 829 continue 830 elif token.is_comment(): 831 self.tok.get_eol() 832 continue 833 elif token.value[0] == '$': 834 u = token.value.upper() 835 if u == '$TTL': 836 token = self.tok.get() 837 if not token.is_identifier(): 838 raise dns.exception.SyntaxError("bad $TTL") 839 self.ttl = dns.ttl.from_text(token.value) 840 self.tok.get_eol() 841 elif u == '$ORIGIN': 842 self.current_origin = self.tok.get_name() 843 self.tok.get_eol() 844 if self.zone.origin is None: 845 self.zone.origin = self.current_origin 846 elif u == '$INCLUDE' and self.allow_include: 847 token = self.tok.get() 848 filename = token.value 849 token = self.tok.get() 850 if token.is_identifier(): 851 new_origin = dns.name.from_text(token.value, \ 852 self.current_origin) 853 self.tok.get_eol() 854 elif not token.is_eol_or_eof(): 855 raise dns.exception.SyntaxError("bad origin in $INCLUDE") 856 else: 857 new_origin = self.current_origin 858 self.saved_state.append((self.tok, 859 self.current_origin, 860 self.last_name, 861 self.current_file, 862 self.ttl)) 863 self.current_file = file(filename, 'r') 864 self.tok = dns.tokenizer.Tokenizer(self.current_file, 865 filename) 866 self.current_origin = new_origin 867 elif u == '$GENERATE': 868 self._generate_line() 869 else: 870 raise dns.exception.SyntaxError("Unknown master file directive '" + u + "'") 871 continue 872 self.tok.unget(token) 873 self._rr_line() 874 except dns.exception.SyntaxError, detail: 875 (filename, line_number) = self.tok.where() 876 if detail is None: 877 detail = "syntax error" 878 raise dns.exception.SyntaxError("%s:%d: %s" % (filename, line_number, detail)) 879 880 # Now that we're done reading, do some basic checking of the zone. 881 if self.check_origin: 882 self.zone.check_origin()
883
884 -def from_text(text, origin = None, rdclass = dns.rdataclass.IN, 885 relativize = True, zone_factory=Zone, filename=None, 886 allow_include=False, check_origin=True):
887 """Build a zone object from a master file format string. 888 889 @param text: the master file format input 890 @type text: string. 891 @param origin: The origin of the zone; if not specified, the first 892 $ORIGIN statement in the master file will determine the origin of the 893 zone. 894 @type origin: dns.name.Name object or string 895 @param rdclass: The zone's rdata class; the default is class IN. 896 @type rdclass: int 897 @param relativize: should names be relativized? The default is True 898 @type relativize: bool 899 @param zone_factory: The zone factory to use 900 @type zone_factory: function returning a Zone 901 @param filename: The filename to emit when describing where an error 902 occurred; the default is '<string>'. 903 @type filename: string 904 @param allow_include: is $INCLUDE allowed? 905 @type allow_include: bool 906 @param check_origin: should sanity checks of the origin node be done? 907 The default is True. 908 @type check_origin: bool 909 @raises dns.zone.NoSOA: No SOA RR was found at the zone origin 910 @raises dns.zone.NoNS: No NS RRset was found at the zone origin 911 @rtype: dns.zone.Zone object 912 """ 913 914 # 'text' can also be a file, but we don't publish that fact 915 # since it's an implementation detail. The official file 916 # interface is from_file(). 917 918 if filename is None: 919 filename = '<string>' 920 tok = dns.tokenizer.Tokenizer(text, filename) 921 reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory, 922 allow_include=allow_include, 923 check_origin=check_origin) 924 reader.read() 925 return reader.zone
926
927 -def from_file(f, origin = None, rdclass = dns.rdataclass.IN, 928 relativize = True, zone_factory=Zone, filename=None, 929 allow_include=True, check_origin=True):
930 """Read a master file and build a zone object. 931 932 @param f: file or string. If I{f} is a string, it is treated 933 as the name of a file to open. 934 @param origin: The origin of the zone; if not specified, the first 935 $ORIGIN statement in the master file will determine the origin of the 936 zone. 937 @type origin: dns.name.Name object or string 938 @param rdclass: The zone's rdata class; the default is class IN. 939 @type rdclass: int 940 @param relativize: should names be relativized? The default is True 941 @type relativize: bool 942 @param zone_factory: The zone factory to use 943 @type zone_factory: function returning a Zone 944 @param filename: The filename to emit when describing where an error 945 occurred; the default is '<file>', or the value of I{f} if I{f} is a 946 string. 947 @type filename: string 948 @param allow_include: is $INCLUDE allowed? 949 @type allow_include: bool 950 @param check_origin: should sanity checks of the origin node be done? 951 The default is True. 952 @type check_origin: bool 953 @raises dns.zone.NoSOA: No SOA RR was found at the zone origin 954 @raises dns.zone.NoNS: No NS RRset was found at the zone origin 955 @rtype: dns.zone.Zone object 956 """ 957 958 if sys.hexversion >= 0x02030000: 959 # allow Unicode filenames; turn on universal newline support 960 str_type = basestring 961 opts = 'rU' 962 else: 963 str_type = str 964 opts = 'r' 965 if isinstance(f, str_type): 966 if filename is None: 967 filename = f 968 f = file(f, opts) 969 want_close = True 970 else: 971 if filename is None: 972 filename = '<file>' 973 want_close = False 974 975 try: 976 z = from_text(f, origin, rdclass, relativize, zone_factory, 977 filename, allow_include, check_origin) 978 finally: 979 if want_close: 980 f.close() 981 return z
982
983 -def from_xfr(xfr, zone_factory=Zone, relativize=True, check_origin=True):
984 """Convert the output of a zone transfer generator into a zone object. 985 986 @param xfr: The xfr generator 987 @type xfr: generator of dns.message.Message objects 988 @param relativize: should names be relativized? The default is True. 989 It is essential that the relativize setting matches the one specified 990 to dns.query.xfr(). 991 @type relativize: bool 992 @param check_origin: should sanity checks of the origin node be done? 993 The default is True. 994 @type check_origin: bool 995 @raises dns.zone.NoSOA: No SOA RR was found at the zone origin 996 @raises dns.zone.NoNS: No NS RRset was found at the zone origin 997 @rtype: dns.zone.Zone object 998 """ 999 1000 z = None 1001 for r in xfr: 1002 if z is None: 1003 if relativize: 1004 origin = r.origin 1005 else: 1006 origin = r.answer[0].name 1007 rdclass = r.answer[0].rdclass 1008 z = zone_factory(origin, rdclass, relativize=relativize) 1009 for rrset in r.answer: 1010 znode = z.nodes.get(rrset.name) 1011 if not znode: 1012 znode = z.node_factory() 1013 z.nodes[rrset.name] = znode 1014 zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype, 1015 rrset.covers, True) 1016 zrds.update_ttl(rrset.ttl) 1017 for rd in rrset: 1018 rd.choose_relativity(z.origin, relativize) 1019 zrds.add(rd) 1020 if check_origin: 1021 z.check_origin() 1022 return z
1023