diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:22 +0000 |
commit | 38b7c80217c4e72b1d8988eb1e60bb6e77334114 (patch) | |
tree | 356e9fd3762877d07cde52d21e77070aeff7e789 /ansible_collections/community/dns/plugins/module_utils | |
parent | Adding upstream version 7.7.0+dfsg. (diff) | |
download | ansible-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')
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. |