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

Source Code for Module dns.zone

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