summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/dns/plugins/module_utils
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-18 05:52:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-18 05:52:22 +0000
commit38b7c80217c4e72b1d8988eb1e60bb6e77334114 (patch)
tree356e9fd3762877d07cde52d21e77070aeff7e789 /ansible_collections/community/dns/plugins/module_utils
parentAdding upstream version 7.7.0+dfsg. (diff)
downloadansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.tar.xz
ansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.zip
Adding upstream version 9.4.0+dfsg.upstream/9.4.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/dns/plugins/module_utils')
-rw-r--r--ansible_collections/community/dns/plugins/module_utils/conversion/txt.py4
-rw-r--r--ansible_collections/community/dns/plugins/module_utils/dnspython_records.py135
-rw-r--r--ansible_collections/community/dns/plugins/module_utils/http.py3
-rw-r--r--ansible_collections/community/dns/plugins/module_utils/ips.py37
-rw-r--r--ansible_collections/community/dns/plugins/module_utils/resolver.py187
-rw-r--r--ansible_collections/community/dns/plugins/module_utils/zone_record_helpers.py2
6 files changed, 328 insertions, 40 deletions
diff --git a/ansible_collections/community/dns/plugins/module_utils/conversion/txt.py b/ansible_collections/community/dns/plugins/module_utils/conversion/txt.py
index a4521c7fa..7258ef58d 100644
--- a/ansible_collections/community/dns/plugins/module_utils/conversion/txt.py
+++ b/ansible_collections/community/dns/plugins/module_utils/conversion/txt.py
@@ -183,6 +183,10 @@ def encode_txt_value(value, always_quote=False, use_character_encoding=_SENTINEL
# Add letter
if letter in (b'"', b'\\'):
+ # Make sure that we don't split up an escape sequence over multiple TXT strings
+ if len(buffer) + 2 > 255:
+ append(buffer[:255])
+ buffer = buffer[255:]
buffer.append(b'\\')
buffer.append(letter)
elif use_character_encoding and not (0x20 <= ord(letter) < 0x7F):
diff --git a/ansible_collections/community/dns/plugins/module_utils/dnspython_records.py b/ansible_collections/community/dns/plugins/module_utils/dnspython_records.py
new file mode 100644
index 000000000..c5bb0b5e4
--- /dev/null
+++ b/ansible_collections/community/dns/plugins/module_utils/dnspython_records.py
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2015, Jan-Piet Mens <jpmens(at)gmail.com>
+# Copyright (c) 2017 Ansible Project
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import base64
+
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.six import binary_type
+
+NAME_TO_RDTYPE = {}
+RDTYPE_TO_NAME = {}
+RDTYPE_TO_FIELDS = {}
+
+try:
+ import dns.name
+ import dns.rdata
+ import dns.rdatatype
+
+ # The following data has been borrowed from community.general's dig lookup plugin.
+ #
+ # Note: adding support for RRSIG is hard work. :)
+ for name, rdtype, fields in [
+ ('A', dns.rdatatype.A, ['address']),
+ ('AAAA', dns.rdatatype.AAAA, ['address']),
+ ('CAA', dns.rdatatype.CAA, ['flags', 'tag', 'value']),
+ ('CNAME', dns.rdatatype.CNAME, ['target']),
+ ('DNAME', dns.rdatatype.DNAME, ['target']),
+ ('DNSKEY', dns.rdatatype.DNSKEY, ['flags', 'algorithm', 'protocol', 'key']),
+ ('DS', dns.rdatatype.DS, ['algorithm', 'digest_type', 'key_tag', 'digest']),
+ ('HINFO', dns.rdatatype.HINFO, ['cpu', 'os']),
+ ('LOC', dns.rdatatype.LOC, ['latitude', 'longitude', 'altitude', 'size', 'horizontal_precision', 'vertical_precision']),
+ ('MX', dns.rdatatype.MX, ['preference', 'exchange']),
+ ('NAPTR', dns.rdatatype.NAPTR, ['order', 'preference', 'flags', 'service', 'regexp', 'replacement']),
+ ('NS', dns.rdatatype.NS, ['target']),
+ ('NSEC', dns.rdatatype.NSEC, ['next', 'windows']),
+ ('NSEC3', dns.rdatatype.NSEC3, ['algorithm', 'flags', 'iterations', 'salt', 'next', 'windows']),
+ ('NSEC3PARAM', dns.rdatatype.NSEC3PARAM, ['algorithm', 'flags', 'iterations', 'salt']),
+ ('PTR', dns.rdatatype.PTR, ['target']),
+ ('RP', dns.rdatatype.RP, ['mbox', 'txt']),
+ ('RRSIG', dns.rdatatype.RRSIG, ['type_covered', 'algorithm', 'labels', 'original_ttl', 'expiration', 'inception', 'key_tag', 'signer', 'signature']),
+ ('SOA', dns.rdatatype.SOA, ['mname', 'rname', 'serial', 'refresh', 'retry', 'expire', 'minimum']),
+ ('SPF', dns.rdatatype.SPF, ['strings']),
+ ('SRV', dns.rdatatype.SRV, ['priority', 'weight', 'port', 'target']),
+ ('SSHFP', dns.rdatatype.SSHFP, ['algorithm', 'fp_type', 'fingerprint']),
+ ('TLSA', dns.rdatatype.TLSA, ['usage', 'selector', 'mtype', 'cert']),
+ ('TXT', dns.rdatatype.TXT, ['strings']),
+ ]:
+ NAME_TO_RDTYPE[name] = rdtype
+ RDTYPE_TO_NAME[rdtype] = name
+ RDTYPE_TO_FIELDS[rdtype] = fields
+
+except ImportError:
+ pass # has to be handled on application level
+
+
+def convert_rdata_to_dict(rdata, to_unicode=True, add_synthetic=True):
+ '''
+ Convert a DNSPython record data object to a Python dictionary.
+
+ Code borrowed from community.general's dig looup plugin.
+
+ If ``to_unicode=True``, all strings will be converted to Unicode/UTF-8 strings.
+
+ If ``add_synthetic=True``, for some record types additional fields are added.
+ For TXT and SPF records, ``value`` contains the concatenated strings, for example.
+ '''
+ result = {}
+
+ fields = RDTYPE_TO_FIELDS.get(rdata.rdtype)
+ if fields is None:
+ raise ValueError('Unsupported record type {rdtype}'.format(rdtype=rdata.rdtype))
+ for f in fields:
+ val = rdata.__getattribute__(f)
+
+ if isinstance(val, dns.name.Name):
+ val = dns.name.Name.to_text(val)
+
+ if rdata.rdtype == dns.rdatatype.DS and f == 'digest':
+ val = dns.rdata._hexify(rdata.digest).replace(' ', '')
+ if rdata.rdtype == dns.rdatatype.DNSKEY and f == 'algorithm':
+ val = int(val)
+ if rdata.rdtype == dns.rdatatype.DNSKEY and f == 'key':
+ val = dns.rdata._base64ify(rdata.key).replace(' ', '')
+ if rdata.rdtype == dns.rdatatype.NSEC3 and f == 'next':
+ val = to_native(base64.b32encode(rdata.next).translate(dns.rdtypes.ANY.NSEC3.b32_normal_to_hex).lower())
+ if rdata.rdtype in (dns.rdatatype.NSEC, dns.rdatatype.NSEC3) and f == 'windows':
+ try:
+ val = dns.rdtypes.util.Bitmap(rdata.windows).to_text().lstrip(' ')
+ except AttributeError:
+ # dnspython < 2.0.0
+ val = []
+ for window, bitmap in rdata.windows:
+ for i, byte in enumerate(bitmap):
+ for j in range(8):
+ if (byte >> (7 - j)) & 1 != 0:
+ val.append(dns.rdatatype.to_text(window * 256 + i * 8 + j))
+ val = ' '.join(val).lstrip(' ')
+ if rdata.rdtype in (dns.rdatatype.NSEC3, dns.rdatatype.NSEC3PARAM) and f == 'salt':
+ val = dns.rdata._hexify(rdata.salt).replace(' ', '')
+ if rdata.rdtype == dns.rdatatype.RRSIG and f == 'type_covered':
+ val = RDTYPE_TO_NAME.get(rdata.type_covered) or str(val)
+ if rdata.rdtype == dns.rdatatype.RRSIG and f == 'algorithm':
+ val = int(val)
+ if rdata.rdtype == dns.rdatatype.RRSIG and f == 'signature':
+ val = dns.rdata._base64ify(rdata.signature).replace(' ', '')
+ if rdata.rdtype == dns.rdatatype.SSHFP and f == 'fingerprint':
+ val = dns.rdata._hexify(rdata.fingerprint).replace(' ', '')
+ if rdata.rdtype == dns.rdatatype.TLSA and f == 'cert':
+ val = dns.rdata._hexify(rdata.cert).replace(' ', '')
+
+ if isinstance(val, (list, tuple)):
+ if to_unicode:
+ val = [to_text(v) if isinstance(v, binary_type) else v for v in val]
+ else:
+ val = list(val)
+ elif to_unicode and isinstance(val, binary_type):
+ val = to_text(val)
+
+ result[f] = val
+
+ if add_synthetic:
+ if rdata.rdtype in (dns.rdatatype.TXT, dns.rdatatype.SPF):
+ if to_unicode:
+ result['value'] = u''.join([to_text(str) for str in rdata.strings])
+ else:
+ result['value'] = b''.join([to_bytes(str) for str in rdata.strings])
+ return result
diff --git a/ansible_collections/community/dns/plugins/module_utils/http.py b/ansible_collections/community/dns/plugins/module_utils/http.py
index fc4e1a590..d904100fc 100644
--- a/ansible_collections/community/dns/plugins/module_utils/http.py
+++ b/ansible_collections/community/dns/plugins/module_utils/http.py
@@ -13,7 +13,8 @@ import abc
from ansible.module_utils import six
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six import PY3
-from ansible.module_utils.urls import fetch_url, open_url, urllib_error, NoSSLError, ConnectionError
+from ansible.module_utils.urls import fetch_url, open_url, NoSSLError, ConnectionError
+import ansible.module_utils.six.moves.urllib.error as urllib_error
class NetworkError(Exception):
diff --git a/ansible_collections/community/dns/plugins/module_utils/ips.py b/ansible_collections/community/dns/plugins/module_utils/ips.py
new file mode 100644
index 000000000..adad9d228
--- /dev/null
+++ b/ansible_collections/community/dns/plugins/module_utils/ips.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2023, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import traceback
+
+from ansible.module_utils.basic import missing_required_lib
+from ansible.module_utils.common.text.converters import to_text
+
+try:
+ import ipaddress
+except ImportError:
+ IPADDRESS_IMPORT_EXC = traceback.format_exc()
+else:
+ IPADDRESS_IMPORT_EXC = None
+
+
+def is_ip_address(server):
+ try:
+ ipaddress.ip_address(to_text(server))
+ return True
+ except ValueError:
+ return False
+
+
+def assert_requirements_present(module):
+ if IPADDRESS_IMPORT_EXC is not None:
+ module.fail_json(
+ msg=missing_required_lib('ipaddress'),
+ exception=IPADDRESS_IMPORT_EXC,
+ )
diff --git a/ansible_collections/community/dns/plugins/module_utils/resolver.py b/ansible_collections/community/dns/plugins/module_utils/resolver.py
index 98f1034e0..280b697d4 100644
--- a/ansible_collections/community/dns/plugins/module_utils/resolver.py
+++ b/ansible_collections/community/dns/plugins/module_utils/resolver.py
@@ -10,7 +10,7 @@ __metaclass__ = type
import traceback
from ansible.module_utils.basic import missing_required_lib
-from ansible.module_utils.common.text.converters import to_text
+from ansible.module_utils.common.text.converters import to_native, to_text
try:
import dns
@@ -27,23 +27,26 @@ else:
DNSPYTHON_IMPORTERROR = None
+_EDNS_SIZE = 1232 # equals dns.message.DEFAULT_EDNS_PAYLOAD; larger values cause problems with Route53 nameservers for me
+
+
class ResolverError(Exception):
pass
-class ResolveDirectlyFromNameServers(object):
- def __init__(self, timeout=10, timeout_retries=3, always_ask_default_resolver=True):
- self.cache = {}
+class _Resolve(object):
+ def __init__(self, timeout=10, timeout_retries=3, servfail_retries=0):
self.timeout = timeout
self.timeout_retries = timeout_retries
+ self.servfail_retries = servfail_retries
self.default_resolver = dns.resolver.get_default_resolver()
- self.default_nameservers = self.default_resolver.nameservers
- self.always_ask_default_resolver = always_ask_default_resolver
- def _handle_reponse_errors(self, target, response, nameserver=None, query=None):
+ def _handle_reponse_errors(self, target, response, nameserver=None, query=None, accept_errors=None):
rcode = response.rcode()
if rcode == dns.rcode.NOERROR:
return True
+ if accept_errors and rcode in accept_errors:
+ return True
if rcode == dns.rcode.NXDOMAIN:
raise dns.resolver.NXDOMAIN(qnames=[target], responses={target: response})
msg = 'Error %s' % dns.rcode.to_text(rcode)
@@ -63,6 +66,96 @@ class ResolveDirectlyFromNameServers(object):
raise exc
retry += 1
+ def _resolve(self, resolver, dnsname, handle_response_errors=False, **kwargs):
+ retry = 0
+ while True:
+ try:
+ response = self._handle_timeout(resolver.resolve, dnsname, lifetime=self.timeout, **kwargs)
+ except AttributeError:
+ # For dnspython < 2.0.0
+ resolver.search = False
+ try:
+ response = self._handle_timeout(resolver.query, dnsname, lifetime=self.timeout, **kwargs)
+ except TypeError:
+ # For dnspython < 1.6.0
+ resolver.lifetime = self.timeout
+ response = self._handle_timeout(resolver.query, dnsname, **kwargs)
+ if response.response.rcode() == dns.rcode.SERVFAIL and retry < self.servfail_retries:
+ retry += 1
+ continue
+ if handle_response_errors:
+ self._handle_reponse_errors(dnsname, response.response, nameserver=resolver.nameservers)
+ return response.rrset
+
+
+class SimpleResolver(_Resolve):
+ def __init__(
+ self,
+ timeout=10,
+ timeout_retries=3,
+ servfail_retries=0,
+ ):
+ super(SimpleResolver, self).__init__(
+ timeout=timeout,
+ timeout_retries=timeout_retries,
+ servfail_retries=servfail_retries,
+ )
+
+ def resolve(self, target, nxdomain_is_empty=True, server_addresses=None, **kwargs):
+ dnsname = dns.name.from_unicode(to_text(target))
+
+ resolver = self.default_resolver
+ if server_addresses:
+ resolver = dns.resolver.Resolver(configure=False)
+ resolver.timeout = self.timeout
+ resolver.nameservers = server_addresses
+
+ resolver.use_edns(0, ednsflags=dns.flags.DO, payload=_EDNS_SIZE)
+
+ try:
+ return self._resolve(resolver, dnsname, handle_response_errors=True, **kwargs)
+ except dns.resolver.NXDOMAIN:
+ if nxdomain_is_empty:
+ return None
+ raise
+ except dns.resolver.NoAnswer:
+ return None
+
+ def resolve_addresses(self, target, **kwargs):
+ dnsname = dns.name.from_unicode(to_text(target))
+ resolver = self.default_resolver
+ result = []
+ try:
+ for data in self._resolve(resolver, dnsname, handle_response_errors=True, rdtype=dns.rdatatype.A, **kwargs):
+ result.append(str(data))
+ except dns.resolver.NoAnswer:
+ pass
+ try:
+ for data in self._resolve(resolver, dnsname, handle_response_errors=True, rdtype=dns.rdatatype.AAAA, **kwargs):
+ result.append(str(data))
+ except dns.resolver.NoAnswer:
+ pass
+ return result
+
+
+class ResolveDirectlyFromNameServers(_Resolve):
+ def __init__(
+ self,
+ timeout=10,
+ timeout_retries=3,
+ servfail_retries=0,
+ always_ask_default_resolver=True,
+ server_addresses=None,
+ ):
+ super(ResolveDirectlyFromNameServers, self).__init__(
+ timeout=timeout,
+ timeout_retries=timeout_retries,
+ servfail_retries=servfail_retries,
+ )
+ self.cache = {}
+ self.default_nameservers = self.default_resolver.nameservers if server_addresses is None else server_addresses
+ self.always_ask_default_resolver = always_ask_default_resolver
+
def _lookup_ns_names(self, target, nameservers=None, nameserver_ips=None):
if self.always_ask_default_resolver:
nameservers = None
@@ -75,8 +168,16 @@ class ResolveDirectlyFromNameServers(object):
raise ResolverError('Have neither nameservers nor nameserver IPs')
query = dns.message.make_query(target, dns.rdatatype.NS)
- response = self._handle_timeout(dns.query.udp, query, nameserver_ips[0], timeout=self.timeout)
- self._handle_reponse_errors(target, response, nameserver=nameserver_ips[0], query='get NS for "%s"' % target)
+ retry = 0
+ while True:
+ response = self._handle_timeout(dns.query.udp, query, nameserver_ips[0], timeout=self.timeout)
+ if response.rcode() == dns.rcode.SERVFAIL and retry < self.servfail_retries:
+ retry += 1
+ continue
+ break
+ self._handle_reponse_errors(
+ target, response, nameserver=nameserver_ips[0], query='get NS for "%s"' % target, accept_errors=[dns.rcode.NXDOMAIN],
+ )
cname = None
for rrset in response.answer:
@@ -96,18 +197,8 @@ class ResolveDirectlyFromNameServers(object):
def _lookup_address_impl(self, target, rdtype):
try:
- try:
- answer = self._handle_timeout(self.default_resolver.resolve, target, rdtype=rdtype, lifetime=self.timeout)
- except AttributeError:
- # For dnspython < 2.0.0
- self.default_resolver.search = False
- try:
- answer = self._handle_timeout(self.default_resolver.query, target, rdtype=rdtype, lifetime=self.timeout)
- except TypeError:
- # For dnspython < 1.6.0
- self.default_resolver.lifetime = self.timeout
- answer = self._handle_timeout(self.default_resolver.query, target, rdtype=rdtype)
- return [str(res) for res in answer.rrset]
+ answer = self._resolve(self.default_resolver, target, handle_response_errors=True, rdtype=rdtype)
+ return [str(res) for res in answer]
except dns.resolver.NoAnswer:
return []
@@ -150,6 +241,7 @@ class ResolveDirectlyFromNameServers(object):
resolver = self.cache.get(cache_index)
if resolver is None:
resolver = dns.resolver.Resolver(configure=False)
+ resolver.use_edns(0, ednsflags=dns.flags.DO, payload=_EDNS_SIZE)
resolver.timeout = self.timeout
nameserver_ips = set()
for nameserver in nameservers:
@@ -162,10 +254,10 @@ class ResolveDirectlyFromNameServers(object):
nameservers = self._lookup_ns(dns.name.from_unicode(to_text(target)))
if resolve_addresses:
nameserver_ips = set()
- for nameserver in nameservers:
+ for nameserver in nameservers or []:
nameserver_ips.update(self._lookup_address(nameserver))
nameservers = list(nameserver_ips)
- return sorted(nameservers)
+ return sorted(nameservers or [])
def resolve(self, target, nxdomain_is_empty=True, **kwargs):
dnsname = dns.name.from_unicode(to_text(target))
@@ -186,28 +278,47 @@ class ResolveDirectlyFromNameServers(object):
loop_catcher.add(dnsname)
results = {}
- for nameserver in nameservers:
+ for nameserver in nameservers or []:
results[nameserver] = None
resolver = self._get_resolver(dnsname, [nameserver])
try:
- try:
- response = self._handle_timeout(resolver.resolve, dnsname, lifetime=self.timeout, **kwargs)
- except AttributeError:
- # For dnspython < 2.0.0
- resolver.search = False
- try:
- response = self._handle_timeout(resolver.query, dnsname, lifetime=self.timeout, **kwargs)
- except TypeError:
- # For dnspython < 1.6.0
- resolver.lifetime = self.timeout
- response = self._handle_timeout(resolver.query, dnsname, **kwargs)
- if response.rrset:
- results[nameserver] = response.rrset
+ results[nameserver] = self._resolve(resolver, dnsname, handle_response_errors=True, **kwargs)
except dns.resolver.NoAnswer:
pass
+ except dns.resolver.NXDOMAIN:
+ if nxdomain_is_empty:
+ results[nameserver] = []
+ else:
+ raise
return results
+def guarded_run(runner, module, server=None, generate_additional_results=None):
+ suffix = ' for {0}'.format(server) if server is not None else ''
+ kwargs = {}
+ try:
+ return runner()
+ except ResolverError as e:
+ if generate_additional_results is not None:
+ kwargs = generate_additional_results()
+ module.fail_json(
+ msg='Unexpected resolving error{0}: {1}'.format(suffix, to_native(e)),
+ exception=traceback.format_exc(),
+ **kwargs
+ )
+ except dns.exception.DNSException as e:
+ if generate_additional_results is not None:
+ kwargs = generate_additional_results()
+ module.fail_json(
+ msg='Unexpected DNS error{0}: {1}'.format(suffix, to_native(e)),
+ exception=traceback.format_exc(),
+ **kwargs
+ )
+
+
def assert_requirements_present(module):
if DNSPYTHON_IMPORTERROR is not None:
- module.fail_json(msg=missing_required_lib('dnspython'), exception=DNSPYTHON_IMPORTERROR)
+ module.fail_json(
+ msg=missing_required_lib('dnspython'),
+ exception=DNSPYTHON_IMPORTERROR,
+ )
diff --git a/ansible_collections/community/dns/plugins/module_utils/zone_record_helpers.py b/ansible_collections/community/dns/plugins/module_utils/zone_record_helpers.py
index 57277e29b..27b62c04d 100644
--- a/ansible_collections/community/dns/plugins/module_utils/zone_record_helpers.py
+++ b/ansible_collections/community/dns/plugins/module_utils/zone_record_helpers.py
@@ -37,7 +37,7 @@ def bulk_apply_changes(api,
@param stop_early_on_errors: If set to ``True``, try to stop changes after the first error happens.
This might only work on some APIs.
@return A tuple (changed, errors, success) where ``changed`` is a boolean which indicates whether a
- change was made, ``errors`` is a list of ``DNSAPIError`` instances for the errors occured,
+ change was made, ``errors`` is a list of ``DNSAPIError`` instances for the errors occurred,
and ``success`` is a dictionary with three lists ``success['deleted']``,
``success['changed']`` and ``success['created']``, which list all records that were deleted,
changed and created, respectively.