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