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