Bump dnspython from 2.0.0 to 2.2.0 (#1618)

* Bump dnspython from 2.0.0 to 2.2.0

Bumps [dnspython]() from 2.0.0 to 2.2.0.

---
updated-dependencies:
- dependency-name: dnspython
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update dnspython==2.2.0

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>

[skip ci]
This commit is contained in:
dependabot[bot] 2022-01-25 11:08:24 -08:00 committed by GitHub
parent 515a5d42d3
commit 3c93b5600f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
143 changed files with 7498 additions and 2054 deletions

View file

@ -18,22 +18,26 @@
"""DNS Zones."""
import contextlib
import hashlib
import io
import os
import re
import sys
import struct
import dns.exception
import dns.immutable
import dns.name
import dns.node
import dns.rdataclass
import dns.rdatatype
import dns.rdata
import dns.rdtypes.ANY.SOA
import dns.rdtypes.ANY.ZONEMD
import dns.rrset
import dns.tokenizer
import dns.transaction
import dns.ttl
import dns.grange
import dns.zonefile
class BadZone(dns.exception.DNSException):
@ -56,7 +60,54 @@ class UnknownOrigin(BadZone):
"""The DNS zone's origin is unknown."""
class Zone:
class UnsupportedDigestScheme(dns.exception.DNSException):
"""The zone digest's scheme is unsupported."""
class UnsupportedDigestHashAlgorithm(dns.exception.DNSException):
"""The zone digest's origin is unsupported."""
class NoDigest(dns.exception.DNSException):
"""The DNS zone has no ZONEMD RRset at its origin."""
class DigestVerificationFailure(dns.exception.DNSException):
"""The ZONEMD digest failed to verify."""
class DigestScheme(dns.enum.IntEnum):
"""ZONEMD Scheme"""
SIMPLE = 1
@classmethod
def _maximum(cls):
return 255
class DigestHashAlgorithm(dns.enum.IntEnum):
"""ZONEMD Hash Algorithm"""
SHA384 = 1
SHA512 = 2
@classmethod
def _maximum(cls):
return 255
_digest_hashers = {
DigestHashAlgorithm.SHA384: hashlib.sha384,
DigestHashAlgorithm.SHA512: hashlib.sha512,
}
class Zone(dns.transaction.TransactionManager):
"""A DNS zone.
@ -77,7 +128,7 @@ class Zone:
*origin* is the origin of the zone. It may be a ``dns.name.Name``,
a ``str``, or ``None``. If ``None``, then the zone's origin will
be set by the first ``$ORIGIN`` line in a masterfile.
be set by the first ``$ORIGIN`` line in a zone file.
*rdclass*, an ``int``, the zone's rdata class; the default is class IN.
@ -150,20 +201,21 @@ class Zone:
return self.nodes.__iter__()
def keys(self):
return self.nodes.keys() # pylint: disable=dict-keys-not-iterating
return self.nodes.keys()
def values(self):
return self.nodes.values() # pylint: disable=dict-values-not-iterating
return self.nodes.values()
def items(self):
return self.nodes.items() # pylint: disable=dict-items-not-iterating
return self.nodes.items()
def get(self, key):
key = self._validate_name(key)
return self.nodes.get(key)
def __contains__(self, other):
return other in self.nodes
def __contains__(self, key):
key = self._validate_name(key)
return key in self.nodes
def find_node(self, name, create=False):
"""Find a node in the zone, possibly creating it.
@ -532,7 +584,8 @@ class Zone:
for rdata in rds:
yield (name, rds.ttl, rdata)
def to_file(self, f, sorted=True, relativize=True, nl=None):
def to_file(self, f, sorted=True, relativize=True, nl=None,
want_comments=False, want_origin=False):
"""Write a zone to a file.
*f*, a file or `str`. If *f* is a string, it is treated
@ -550,6 +603,14 @@ class Zone:
*nl*, a ``str`` or None. The end of line string. If not
``None``, the output will use the platform's native
end-of-line marker (i.e. LF on POSIX, CRLF on Windows).
*want_comments*, a ``bool``. If ``True``, emit end-of-line comments
as part of writing the file. If ``False``, the default, do not
emit them.
*want_origin*, a ``bool``. If ``True``, emit a $ORIGIN line at
the start of the file. If ``False``, the default, do not emit
one.
"""
with contextlib.ExitStack() as stack:
@ -572,6 +633,16 @@ class Zone:
nl_b = nl
nl = nl.decode()
if want_origin:
l = '$ORIGIN ' + self.origin.to_text()
l_b = l.encode(file_enc)
try:
f.write(l_b)
f.write(nl_b)
except TypeError: # textual mode
f.write(l)
f.write(nl)
if sorted:
names = list(self.keys())
names.sort()
@ -579,12 +650,9 @@ class Zone:
names = self.keys()
for n in names:
l = self[n].to_text(n, origin=self.origin,
relativize=relativize)
if isinstance(l, str):
l_b = l.encode(file_enc)
else:
l_b = l
l = l.decode()
relativize=relativize,
want_comments=want_comments)
l_b = l.encode(file_enc)
try:
f.write(l_b)
@ -593,7 +661,8 @@ class Zone:
f.write(l)
f.write(nl)
def to_text(self, sorted=True, relativize=True, nl=None):
def to_text(self, sorted=True, relativize=True, nl=None,
want_comments=False, want_origin=False):
"""Return a zone's text as though it were written to a file.
*sorted*, a ``bool``. If True, the default, then the file
@ -609,10 +678,19 @@ class Zone:
``None``, the output will use the platform's native
end-of-line marker (i.e. LF on POSIX, CRLF on Windows).
*want_comments*, a ``bool``. If ``True``, emit end-of-line comments
as part of writing the file. If ``False``, the default, do not
emit them.
*want_origin*, a ``bool``. If ``True``, emit a $ORIGIN line at
the start of the output. If ``False``, the default, do not emit
one.
Returns a ``str``.
"""
temp_buffer = io.StringIO()
self.to_file(temp_buffer, sorted, relativize, nl)
self.to_file(temp_buffer, sorted, relativize, nl, want_comments,
want_origin)
return_value = temp_buffer.getvalue()
temp_buffer.close()
return return_value
@ -635,425 +713,334 @@ class Zone:
if self.get_rdataset(name, dns.rdatatype.NS) is None:
raise NoNS
def _compute_digest(self, hash_algorithm, scheme=DigestScheme.SIMPLE):
hashinfo = _digest_hashers.get(hash_algorithm)
if not hashinfo:
raise UnsupportedDigestHashAlgorithm
if scheme != DigestScheme.SIMPLE:
raise UnsupportedDigestScheme
class _MasterReader:
"""Read a DNS master file
@ivar tok: The tokenizer
@type tok: dns.tokenizer.Tokenizer object
@ivar last_ttl: The last seen explicit TTL for an RR
@type last_ttl: int
@ivar last_ttl_known: Has last TTL been detected
@type last_ttl_known: bool
@ivar default_ttl: The default TTL from a $TTL directive or SOA RR
@type default_ttl: int
@ivar default_ttl_known: Has default TTL been detected
@type default_ttl_known: bool
@ivar last_name: The last name read
@type last_name: dns.name.Name object
@ivar current_origin: The current origin
@type current_origin: dns.name.Name object
@ivar relativize: should names in the zone be relativized?
@type relativize: bool
@ivar zone: the zone
@type zone: dns.zone.Zone object
@ivar saved_state: saved reader state (used when processing $INCLUDE)
@type saved_state: list of (tokenizer, current_origin, last_name, file,
last_ttl, last_ttl_known, default_ttl, default_ttl_known) tuples.
@ivar current_file: the file object of the $INCLUDed file being parsed
(None if no $INCLUDE is active).
@ivar allow_include: is $INCLUDE allowed?
@type allow_include: bool
@ivar check_origin: should sanity checks of the origin node be done?
The default is True.
@type check_origin: bool
"""
def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone,
allow_include=False, check_origin=True):
if isinstance(origin, str):
origin = dns.name.from_text(origin)
self.tok = tok
self.current_origin = origin
self.relativize = relativize
self.last_ttl = 0
self.last_ttl_known = False
self.default_ttl = 0
self.default_ttl_known = False
self.last_name = self.current_origin
self.zone = zone_factory(origin, rdclass, relativize=relativize)
self.saved_state = []
self.current_file = None
self.allow_include = allow_include
self.check_origin = check_origin
def _eat_line(self):
while 1:
token = self.tok.get()
if token.is_eol_or_eof():
break
def _rr_line(self):
"""Process one line from a DNS master file."""
# Name
if self.current_origin is None:
raise UnknownOrigin
token = self.tok.get(want_leading=True)
if not token.is_whitespace():
self.last_name = self.tok.as_name(token, self.current_origin)
else:
token = self.tok.get()
if token.is_eol_or_eof():
# treat leading WS followed by EOL/EOF as if they were EOL/EOF.
return
self.tok.unget(token)
name = self.last_name
if not name.is_subdomain(self.zone.origin):
self._eat_line()
return
if self.relativize:
name = name.relativize(self.zone.origin)
token = self.tok.get()
if not token.is_identifier():
raise dns.exception.SyntaxError
origin_name = dns.name.empty
else:
origin_name = self.origin
hasher = hashinfo()
for (name, node) in sorted(self.items()):
rrnamebuf = name.to_digestable(self.origin)
for rdataset in sorted(node,
key=lambda rds: (rds.rdtype, rds.covers)):
if name == origin_name and \
dns.rdatatype.ZONEMD in (rdataset.rdtype, rdataset.covers):
continue
rrfixed = struct.pack('!HHI', rdataset.rdtype,
rdataset.rdclass, rdataset.ttl)
rdatas = [rdata.to_digestable(self.origin)
for rdata in rdataset]
for rdata in sorted(rdatas):
rrlen = struct.pack('!H', len(rdata))
hasher.update(rrnamebuf + rrfixed + rrlen + rdata)
return hasher.digest()
# TTL
ttl = None
try:
ttl = dns.ttl.from_text(token.value)
self.last_ttl = ttl
self.last_ttl_known = True
token = self.tok.get()
if not token.is_identifier():
raise dns.exception.SyntaxError
except dns.ttl.BadTTL:
if self.default_ttl_known:
ttl = self.default_ttl
elif self.last_ttl_known:
ttl = self.last_ttl
def compute_digest(self, hash_algorithm, scheme=DigestScheme.SIMPLE):
if self.relativize:
origin_name = dns.name.empty
else:
origin_name = self.origin
serial = self.get_rdataset(origin_name, dns.rdatatype.SOA)[0].serial
digest = self._compute_digest(hash_algorithm, scheme)
return dns.rdtypes.ANY.ZONEMD.ZONEMD(self.rdclass,
dns.rdatatype.ZONEMD,
serial, scheme, hash_algorithm,
digest)
# Class
try:
rdclass = dns.rdataclass.from_text(token.value)
token = self.tok.get()
if not token.is_identifier():
raise dns.exception.SyntaxError
except dns.exception.SyntaxError:
raise
except Exception:
rdclass = self.zone.rdclass
if rdclass != self.zone.rdclass:
raise dns.exception.SyntaxError("RR class is not zone's class")
# Type
try:
rdtype = dns.rdatatype.from_text(token.value)
except Exception:
raise dns.exception.SyntaxError(
"unknown rdatatype '%s'" % token.value)
n = self.zone.nodes.get(name)
if n is None:
n = self.zone.node_factory()
self.zone.nodes[name] = n
try:
rd = dns.rdata.from_text(rdclass, rdtype, self.tok,
self.current_origin, self.relativize,
self.zone.origin)
except dns.exception.SyntaxError:
# Catch and reraise.
raise
except Exception:
# All exceptions that occur in the processing of rdata
# are treated as syntax errors. This is not strictly
# correct, but it is correct almost all of the time.
# We convert them to syntax errors so that we can emit
# helpful filename:line info.
(ty, va) = sys.exc_info()[:2]
raise dns.exception.SyntaxError(
"caught exception {}: {}".format(str(ty), str(va)))
if not self.default_ttl_known and rdtype == dns.rdatatype.SOA:
# The pre-RFC2308 and pre-BIND9 behavior inherits the zone default
# TTL from the SOA minttl if no $TTL statement is present before the
# SOA is parsed.
self.default_ttl = rd.minimum
self.default_ttl_known = True
if ttl is None:
# if we didn't have a TTL on the SOA, set it!
ttl = rd.minimum
# TTL check. We had to wait until now to do this as the SOA RR's
# own TTL can be inferred from its minimum.
if ttl is None:
raise dns.exception.SyntaxError("Missing default TTL value")
covers = rd.covers()
rds = n.find_rdataset(rdclass, rdtype, covers, True)
rds.add(rd, ttl)
def _parse_modify(self, side):
# Here we catch everything in '{' '}' in a group so we can replace it
# with ''.
is_generate1 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+),(.)}).*$")
is_generate2 = re.compile(r"^.*\$({(\+|-?)(\d+)}).*$")
is_generate3 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+)}).*$")
# Sometimes there are modifiers in the hostname. These come after
# the dollar sign. They are in the form: ${offset[,width[,base]]}.
# Make names
g1 = is_generate1.match(side)
if g1:
mod, sign, offset, width, base = g1.groups()
if sign == '':
sign = '+'
g2 = is_generate2.match(side)
if g2:
mod, sign, offset = g2.groups()
if sign == '':
sign = '+'
width = 0
base = 'd'
g3 = is_generate3.match(side)
if g3:
mod, sign, offset, width = g3.groups()
if sign == '':
sign = '+'
base = 'd'
if not (g1 or g2 or g3):
mod = ''
sign = '+'
offset = 0
width = 0
base = 'd'
if base != 'd':
raise NotImplementedError()
return mod, sign, offset, width, base
def _generate_line(self):
# range lhs [ttl] [class] type rhs [ comment ]
"""Process one line containing the GENERATE statement from a DNS
master file."""
if self.current_origin is None:
raise UnknownOrigin
token = self.tok.get()
# Range (required)
try:
start, stop, step = dns.grange.from_text(token.value)
token = self.tok.get()
if not token.is_identifier():
raise dns.exception.SyntaxError
except Exception:
raise dns.exception.SyntaxError
# lhs (required)
try:
lhs = token.value
token = self.tok.get()
if not token.is_identifier():
raise dns.exception.SyntaxError
except Exception:
raise dns.exception.SyntaxError
# TTL
try:
ttl = dns.ttl.from_text(token.value)
self.last_ttl = ttl
self.last_ttl_known = True
token = self.tok.get()
if not token.is_identifier():
raise dns.exception.SyntaxError
except dns.ttl.BadTTL:
if not (self.last_ttl_known or self.default_ttl_known):
raise dns.exception.SyntaxError("Missing default TTL value")
if self.default_ttl_known:
ttl = self.default_ttl
elif self.last_ttl_known:
ttl = self.last_ttl
# Class
try:
rdclass = dns.rdataclass.from_text(token.value)
token = self.tok.get()
if not token.is_identifier():
raise dns.exception.SyntaxError
except dns.exception.SyntaxError:
raise dns.exception.SyntaxError
except Exception:
rdclass = self.zone.rdclass
if rdclass != self.zone.rdclass:
raise dns.exception.SyntaxError("RR class is not zone's class")
# Type
try:
rdtype = dns.rdatatype.from_text(token.value)
token = self.tok.get()
if not token.is_identifier():
raise dns.exception.SyntaxError
except Exception:
raise dns.exception.SyntaxError("unknown rdatatype '%s'" %
token.value)
# rhs (required)
rhs = token.value
lmod, lsign, loffset, lwidth, lbase = self._parse_modify(lhs)
rmod, rsign, roffset, rwidth, rbase = self._parse_modify(rhs)
for i in range(start, stop + 1, step):
# +1 because bind is inclusive and python is exclusive
if lsign == '+':
lindex = i + int(loffset)
elif lsign == '-':
lindex = i - int(loffset)
if rsign == '-':
rindex = i - int(roffset)
elif rsign == '+':
rindex = i + int(roffset)
lzfindex = str(lindex).zfill(int(lwidth))
rzfindex = str(rindex).zfill(int(rwidth))
name = lhs.replace('$%s' % (lmod), lzfindex)
rdata = rhs.replace('$%s' % (rmod), rzfindex)
self.last_name = dns.name.from_text(name, self.current_origin,
self.tok.idna_codec)
name = self.last_name
if not name.is_subdomain(self.zone.origin):
self._eat_line()
return
if self.relativize:
name = name.relativize(self.zone.origin)
n = self.zone.nodes.get(name)
if n is None:
n = self.zone.node_factory()
self.zone.nodes[name] = n
def verify_digest(self, zonemd=None):
if zonemd:
digests = [zonemd]
else:
digests = self.get_rdataset(self.origin, dns.rdatatype.ZONEMD)
if digests is None:
raise NoDigest
for digest in digests:
try:
rd = dns.rdata.from_text(rdclass, rdtype, rdata,
self.current_origin, self.relativize,
self.zone.origin)
except dns.exception.SyntaxError:
# Catch and reraise.
raise
computed = self._compute_digest(digest.hash_algorithm,
digest.scheme)
if computed == digest.digest:
return
except Exception:
# All exceptions that occur in the processing of rdata
# are treated as syntax errors. This is not strictly
# correct, but it is correct almost all of the time.
# We convert them to syntax errors so that we can emit
# helpful filename:line info.
(ty, va) = sys.exc_info()[:2]
raise dns.exception.SyntaxError("caught exception %s: %s" %
(str(ty), str(va)))
pass
raise DigestVerificationFailure
covers = rd.covers()
rds = n.find_rdataset(rdclass, rdtype, covers, True)
rds.add(rd, ttl)
# TransactionManager methods
def read(self):
"""Read a DNS master file and build a zone object.
def reader(self):
return Transaction(self, False,
Version(self, 1, self.nodes, self.origin))
@raises dns.zone.NoSOA: No SOA RR was found at the zone origin
@raises dns.zone.NoNS: No NS RRset was found at the zone origin
"""
def writer(self, replacement=False):
txn = Transaction(self, replacement)
txn._setup_version()
return txn
try:
while 1:
token = self.tok.get(True, True)
if token.is_eof():
if self.current_file is not None:
self.current_file.close()
if len(self.saved_state) > 0:
(self.tok,
self.current_origin,
self.last_name,
self.current_file,
self.last_ttl,
self.last_ttl_known,
self.default_ttl,
self.default_ttl_known) = self.saved_state.pop(-1)
continue
break
elif token.is_eol():
continue
elif token.is_comment():
self.tok.get_eol()
continue
elif token.value[0] == '$':
c = token.value.upper()
if c == '$TTL':
token = self.tok.get()
if not token.is_identifier():
raise dns.exception.SyntaxError("bad $TTL")
self.default_ttl = dns.ttl.from_text(token.value)
self.default_ttl_known = True
self.tok.get_eol()
elif c == '$ORIGIN':
self.current_origin = self.tok.get_name()
self.tok.get_eol()
if self.zone.origin is None:
self.zone.origin = self.current_origin
elif c == '$INCLUDE' and self.allow_include:
token = self.tok.get()
filename = token.value
token = self.tok.get()
if token.is_identifier():
new_origin =\
dns.name.from_text(token.value,
self.current_origin,
self.tok.idna_codec)
self.tok.get_eol()
elif not token.is_eol_or_eof():
raise dns.exception.SyntaxError(
"bad origin in $INCLUDE")
else:
new_origin = self.current_origin
self.saved_state.append((self.tok,
self.current_origin,
self.last_name,
self.current_file,
self.last_ttl,
self.last_ttl_known,
self.default_ttl,
self.default_ttl_known))
self.current_file = open(filename, 'r')
self.tok = dns.tokenizer.Tokenizer(self.current_file,
filename)
self.current_origin = new_origin
elif c == '$GENERATE':
self._generate_line()
else:
raise dns.exception.SyntaxError(
"Unknown master file directive '" + c + "'")
continue
self.tok.unget(token)
self._rr_line()
except dns.exception.SyntaxError as detail:
(filename, line_number) = self.tok.where()
if detail is None:
detail = "syntax error"
ex = dns.exception.SyntaxError(
"%s:%d: %s" % (filename, line_number, detail))
tb = sys.exc_info()[2]
raise ex.with_traceback(tb) from None
def origin_information(self):
if self.relativize:
effective = dns.name.empty
else:
effective = self.origin
return (self.origin, self.relativize, effective)
# Now that we're done reading, do some basic checking of the zone.
if self.check_origin:
self.zone.check_origin()
def get_class(self):
return self.rdclass
# Transaction methods
def _end_read(self, txn):
pass
def _end_write(self, txn):
pass
def _commit_version(self, _, version, origin):
self.nodes = version.nodes
if self.origin is None:
self.origin = origin
def _get_next_version_id(self):
# Versions are ephemeral and all have id 1
return 1
# These classes used to be in dns.versioned, but have moved here so we can use
# the copy-on-write transaction mechanism for both kinds of zones. In a
# regular zone, the version only exists during the transaction, and the nodes
# are regular dns.node.Nodes.
# A node with a version id.
class VersionedNode(dns.node.Node):
__slots__ = ['id']
def __init__(self):
super().__init__()
# A proper id will get set by the Version
self.id = 0
@dns.immutable.immutable
class ImmutableVersionedNode(VersionedNode):
__slots__ = ['id']
def __init__(self, node):
super().__init__()
self.id = node.id
self.rdatasets = tuple(
[dns.rdataset.ImmutableRdataset(rds) for rds in node.rdatasets]
)
def find_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE,
create=False):
if create:
raise TypeError("immutable")
return super().find_rdataset(rdclass, rdtype, covers, False)
def get_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE,
create=False):
if create:
raise TypeError("immutable")
return super().get_rdataset(rdclass, rdtype, covers, False)
def delete_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE):
raise TypeError("immutable")
def replace_rdataset(self, replacement):
raise TypeError("immutable")
def is_immutable(self):
return True
class Version:
def __init__(self, zone, id, nodes=None, origin=None):
self.zone = zone
self.id = id
if nodes is not None:
self.nodes = nodes
else:
self.nodes = {}
self.origin = origin
def _validate_name(self, name):
if name.is_absolute():
if not name.is_subdomain(self.zone.origin):
raise KeyError("name is not a subdomain of the zone origin")
if self.zone.relativize:
# XXXRTH should it be an error if self.origin is still None?
name = name.relativize(self.origin)
return name
def get_node(self, name):
name = self._validate_name(name)
return self.nodes.get(name)
def get_rdataset(self, name, rdtype, covers):
node = self.get_node(name)
if node is None:
return None
return node.get_rdataset(self.zone.rdclass, rdtype, covers)
def items(self):
return self.nodes.items()
class WritableVersion(Version):
def __init__(self, zone, replacement=False):
# The zone._versions_lock must be held by our caller in a versioned
# zone.
id = zone._get_next_version_id()
super().__init__(zone, id)
if not replacement:
# We copy the map, because that gives us a simple and thread-safe
# way of doing versions, and we have a garbage collector to help
# us. We only make new node objects if we actually change the
# node.
self.nodes.update(zone.nodes)
# We have to copy the zone origin as it may be None in the first
# version, and we don't want to mutate the zone until we commit.
self.origin = zone.origin
self.changed = set()
def _maybe_cow(self, name):
name = self._validate_name(name)
node = self.nodes.get(name)
if node is None or name not in self.changed:
new_node = self.zone.node_factory()
if hasattr(new_node, 'id'):
# We keep doing this for backwards compatibility, as earlier
# code used new_node.id != self.id for the "do we need to CoW?"
# test. Now we use the changed set as this works with both
# regular zones and versioned zones.
new_node.id = self.id
if node is not None:
# moo! copy on write!
new_node.rdatasets.extend(node.rdatasets)
self.nodes[name] = new_node
self.changed.add(name)
return new_node
else:
return node
def delete_node(self, name):
name = self._validate_name(name)
if name in self.nodes:
del self.nodes[name]
self.changed.add(name)
def put_rdataset(self, name, rdataset):
node = self._maybe_cow(name)
node.replace_rdataset(rdataset)
def delete_rdataset(self, name, rdtype, covers):
node = self._maybe_cow(name)
node.delete_rdataset(self.zone.rdclass, rdtype, covers)
if len(node) == 0:
del self.nodes[name]
@dns.immutable.immutable
class ImmutableVersion(Version):
def __init__(self, version):
# We tell super() that it's a replacement as we don't want it
# to copy the nodes, as we're about to do that with an
# immutable Dict.
super().__init__(version.zone, True)
# set the right id!
self.id = version.id
# keep the origin
self.origin = version.origin
# Make changed nodes immutable
for name in version.changed:
node = version.nodes.get(name)
# it might not exist if we deleted it in the version
if node:
version.nodes[name] = ImmutableVersionedNode(node)
self.nodes = dns.immutable.Dict(version.nodes, True)
class Transaction(dns.transaction.Transaction):
def __init__(self, zone, replacement, version=None, make_immutable=False):
read_only = version is not None
super().__init__(zone, replacement, read_only)
self.version = version
self.make_immutable = make_immutable
@property
def zone(self):
return self.manager
def _setup_version(self):
assert self.version is None
self.version = WritableVersion(self.zone, self.replacement)
def _get_rdataset(self, name, rdtype, covers):
return self.version.get_rdataset(name, rdtype, covers)
def _put_rdataset(self, name, rdataset):
assert not self.read_only
self.version.put_rdataset(name, rdataset)
def _delete_name(self, name):
assert not self.read_only
self.version.delete_node(name)
def _delete_rdataset(self, name, rdtype, covers):
assert not self.read_only
self.version.delete_rdataset(name, rdtype, covers)
def _name_exists(self, name):
return self.version.get_node(name) is not None
def _changed(self):
if self.read_only:
return False
else:
return len(self.version.changed) > 0
def _end_transaction(self, commit):
if self.read_only:
self.zone._end_read(self)
elif commit and len(self.version.changed) > 0:
if self.make_immutable:
version = ImmutableVersion(self.version)
else:
version = self.version
self.zone._commit_version(self, version, self.version.origin)
else:
# rollback
self.zone._end_write(self)
def _set_origin(self, origin):
if self.version.origin is None:
self.version.origin = origin
def _iterate_rdatasets(self):
for (name, node) in self.version.items():
for rdataset in node:
yield (name, rdataset)
def _get_node(self, name):
return self.version.get_node(name)
def from_text(text, origin=None, rdclass=dns.rdataclass.IN,
relativize=True, zone_factory=Zone, filename=None,
allow_include=False, check_origin=True, idna_codec=None):
"""Build a zone object from a master file format string.
"""Build a zone object from a zone file format string.
*text*, a ``str``, the master file format input.
*text*, a ``str``, the zone file format input.
*origin*, a ``dns.name.Name``, a ``str``, or ``None``. The origin
of the zone; if not specified, the first ``$ORIGIN`` statement in the
masterfile will determine the origin of the zone.
zone file will determine the origin of the zone.
*rdclass*, an ``int``, the zone's rdata class; the default is class IN.
@ -1094,25 +1081,33 @@ def from_text(text, origin=None, rdclass=dns.rdataclass.IN,
if filename is None:
filename = '<string>'
tok = dns.tokenizer.Tokenizer(text, filename, idna_codec=idna_codec)
reader = _MasterReader(tok, origin, rdclass, relativize, zone_factory,
allow_include=allow_include,
check_origin=check_origin)
reader.read()
return reader.zone
zone = zone_factory(origin, rdclass, relativize=relativize)
with zone.writer(True) as txn:
tok = dns.tokenizer.Tokenizer(text, filename, idna_codec=idna_codec)
reader = dns.zonefile.Reader(tok, rdclass, txn,
allow_include=allow_include)
try:
reader.read()
except dns.zonefile.UnknownOrigin:
# for backwards compatibility
raise dns.zone.UnknownOrigin
# Now that we're done reading, do some basic checking of the zone.
if check_origin:
zone.check_origin()
return zone
def from_file(f, origin=None, rdclass=dns.rdataclass.IN,
relativize=True, zone_factory=Zone, filename=None,
allow_include=True, check_origin=True):
"""Read a master file and build a zone object.
"""Read a zone file and build a zone object.
*f*, a file or ``str``. If *f* is a string, it is treated
as the name of a file to open.
*origin*, a ``dns.name.Name``, a ``str``, or ``None``. The origin
of the zone; if not specified, the first ``$ORIGIN`` statement in the
masterfile will determine the origin of the zone.
zone file will determine the origin of the zone.
*rdclass*, an ``int``, the zone's rdata class; the default is class IN.