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