From 62af104df484d18784acc8313b54f0490eb0e9eb Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 29 Jan 2023 10:53:12 +0100 Subject: [PATCH 1/4] Add nameserver_info and nameserver_record_info modules. --- plugins/module_utils/dnspython_records.py | 135 +++++ plugins/modules/nameserver_info.py | 169 ++++++ plugins/modules/nameserver_record_info.py | 568 ++++++++++++++++++ .../module_utils/test_dnspython_records.py | 408 +++++++++++++ .../plugins/modules/test_nameserver_info.py | 351 +++++++++++ .../modules/test_nameserver_record_info.py | 541 +++++++++++++++++ 6 files changed, 2172 insertions(+) create mode 100644 plugins/module_utils/dnspython_records.py create mode 100644 plugins/modules/nameserver_info.py create mode 100644 plugins/modules/nameserver_record_info.py create mode 100644 tests/unit/plugins/module_utils/test_dnspython_records.py create mode 100644 tests/unit/plugins/modules/test_nameserver_info.py create mode 100644 tests/unit/plugins/modules/test_nameserver_record_info.py diff --git a/plugins/module_utils/dnspython_records.py b/plugins/module_utils/dnspython_records.py new file mode 100644 index 00000000..c5bb0b5e --- /dev/null +++ b/plugins/module_utils/dnspython_records.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2015, Jan-Piet Mens +# Copyright (c) 2017 Ansible Project +# Copyright (c) 2022, Felix Fontein +# 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/plugins/modules/nameserver_info.py b/plugins/modules/nameserver_info.py new file mode 100644 index 00000000..3ed08888 --- /dev/null +++ b/plugins/modules/nameserver_info.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein +# 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 + + +DOCUMENTATION = r''' +--- +module: nameserver_info +short_description: Look up nameservers for a DNS name +version_added: 2.6.0 +description: + - Retrieve all nameservers that are responsible for a DNS name. +extends_documentation_fragment: + - community.dns.attributes + - community.dns.attributes.info_module +author: + - Felix Fontein (@felixfontein) +options: + name: + description: + - A list of DNS names whose nameservers to retrieve. + required: true + type: list + elements: str + resolve_addresses: + description: + - Whether to resolve the nameserver names to IP addresses. + type: bool + default: false + query_retry: + description: + - Number of retries for DNS query timeouts. + type: int + default: 3 + query_timeout: + description: + - Timeout per DNS query in seconds. + type: float + default: 10 + always_ask_default_resolver: + description: + - When set to V(true) (default), will use the default resolver to find the authoritative nameservers + of a subzone. + - When set to V(false), will use the authoritative nameservers of the parent zone to find the + authoritative nameservers of a subzone. This only makes sense when the nameservers were recently + changed and haven't propagated. + type: bool + default: true +requirements: + - dnspython >= 1.15.0 (maybe older versions also work) +''' + +EXAMPLES = r''' +- name: Retrieve name servers of two DNS names + community.dns.nameserver_info: + name: + - www.example.com + - example.org + register: result + +- name: Show nameservers for www.example.com + ansible.builtin.debug: + msg: '{{ result.results[0].nameserver }}' +''' + +RETURN = r''' +results: + description: + - Information on the nameservers for every DNS name provided in O(name). + returned: always + type: list + elements: dict + contains: + name: + description: + - The DNS name this entry is for. + returned: always + type: str + sample: www.example.com + nameservers: + description: + - A list of nameservers for this DNS name. + returned: success + type: list + elements: str + sample: + - ns1.example.com + - ns2.example.com + sample: + - name: www.example.com + nameservers: + - ns1.example.com + - ns2.example.com + - name: example.org + nameservers: + - ns1.example.org + - ns2.example.org + - ns3.example.org +''' + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.dns.plugins.module_utils.resolver import ( + ResolveDirectlyFromNameServers, + ResolverError, + assert_requirements_present, +) + +try: + import dns.exception + import dns.rdatatype +except ImportError: + pass # handled in assert_requirements_present() + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True, type='list', elements='str'), + resolve_addresses=dict(type='bool', default=False), + query_retry=dict(type='int', default=3), + query_timeout=dict(type='float', default=10), + always_ask_default_resolver=dict(type='bool', default=True), + ), + supports_check_mode=True, + ) + assert_requirements_present(module) + + names = module.params['name'] + resolve_addresses = module.params['resolve_addresses'] + + resolver = ResolveDirectlyFromNameServers( + timeout=module.params['query_timeout'], + timeout_retries=module.params['query_retry'], + always_ask_default_resolver=module.params['always_ask_default_resolver'], + ) + results = [None] * len(names) + for index, name in enumerate(names): + results[index] = { + 'name': name, + } + + try: + for index, name in enumerate(names): + results[index]['nameservers'] = sorted(resolver.resolve_nameservers(name, resolve_addresses=resolve_addresses)) + module.exit_json(results=results) + except ResolverError as e: + module.fail_json( + msg='Unexpected resolving error: {0}'.format(to_native(e)), + results=results, + exception=traceback.format_exc()) + except dns.exception.DNSException as e: + module.fail_json( + msg='Unexpected DNS error: {0}'.format(to_native(e)), + results=results, + exception=traceback.format_exc()) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/nameserver_record_info.py b/plugins/modules/nameserver_record_info.py new file mode 100644 index 00000000..ced82a27 --- /dev/null +++ b/plugins/modules/nameserver_record_info.py @@ -0,0 +1,568 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein +# 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 + + +DOCUMENTATION = r''' +--- +module: nameserver_record_info +short_description: Look up all records of a type from all nameservers for a DNS name +version_added: 2.6.0 +description: + - Given a DNS name and a record type, will retrieve all nameservers that are responsible for + this DNS name, and from them all records for this name of the given type. +extends_documentation_fragment: + - community.dns.attributes + - community.dns.attributes.info_module +author: + - Felix Fontein (@felixfontein) +options: + name: + description: + - A list of DNS names whose nameservers to retrieve. + required: true + type: list + elements: str + type: + description: + - The record type to retrieve. + required: true + type: str + choices: + - A + - ALL + - AAAA + - CAA + - CNAME + - DNAME + - DNSKEY + - DS + - HINFO + - LOC + - MX + - NAPTR + - NS + - NSEC + - NSEC3 + - NSEC3PARAM + - PTR + - RP + - RRSIG + - SOA + - SPF + - SRV + - SSHFP + - TLSA + - TXT + query_retry: + description: + - Number of retries for DNS query timeouts. + type: int + default: 3 + query_timeout: + description: + - Timeout per DNS query in seconds. + type: float + default: 10 + always_ask_default_resolver: + description: + - When set to V(true) (default), will use the default resolver to find the authoritative nameservers + of a subzone. + - When set to V(false), will use the authoritative nameservers of the parent zone to find the + authoritative nameservers of a subzone. This only makes sense when the nameservers were recently + changed and haven't propagated. + type: bool + default: true +requirements: + - dnspython >= 1.15.0 (maybe older versions also work) +notes: + - dnspython before 2.0.0 does not correctly support (un-)escaping UTF-8 in TXT-like records. This can + result in wrongly decoded TXT records. Please use dnspython 2.0.0 or later to fix this issue; see also + U(https://github.com/rthalley/dnspython/issues/321). + Unfortunately dnspython 2.0.0 requires Python 3.6 or newer. +''' + +EXAMPLES = r''' +- name: Retrieve TXT values from all nameservers for two DNS names + community.dns.nameserver_record_info: + name: + - www.example.com + - example.org + type: TXT + register: result + +- name: Show TXT values for www.example.com for all nameservers + ansible.builtin.debug: + msg: '{{ result.results[0].result }}' +''' + +RETURN = r''' +results: + description: + - Information on the records for every DNS name provided in O(name). + returned: always + type: list + elements: dict + contains: + name: + description: + - The DNS name this entry is for. + returned: always + type: str + sample: www.example.com + result: + description: + - A list of values per nameserver. + returned: success + type: list + elements: dict + sample: + - nameserver: ns1.example.com + values: + - X + - nameserver: ns2.example.com + values: + - X + contains: + nameserver: + description: + - The nameserver. + returned: success + type: str + sample: ns1.example.com + values: + description: + - The records of type O(type). + - Depending on O(type), different fields are returned. + - For O(type=TXT) and O(type=SPF), also the concatenated value is returned as C(values). + returned: success + type: list + elements: dict + sample: + - address: 127.0.0.1 + contains: + address: + description: + - A IPv4 respectively IPv6 address. + type: str + returned: O(type=A) or O(type=AAAA) + algorithm: + description: + - The algorithm ID. + type: int + returned: O(type=DNSKEY) or O(type=DS) or O(type=NSEC3) or O(type=NSEC3PARAM) or O(type=RRSIG) or O(type=SSHFP) + altitude: + description: + - The altitute. + type: float + returned: O(type=LOC) + cert: + description: + - The certificate. + type: str + returned: O(type=TLSA) + cpu: + description: + - The CPU. + type: str + returned: O(type=HINFO) + digest: + description: + - The digest. + type: str + returned: O(type=DS) + digest_type: + description: + - The digest's type. + type: int + returned: O(type=DS) + exchange: + description: + - The exchange server. + type: str + returned: O(type=MX) + expiration: + description: + - The expiration Unix timestamp. + type: int + returned: O(type=RRSIG) + expire: + description: + - Number of seconds after which secondary name servers should stop answering request + for this zone if the main name server does not respond. + type: int + returned: O(type=SOA) + fingerprint: + description: + - The fingerprint. + type: str + returned: O(type=SSHFP) + flags: + description: + - Flags. + - This is actually of type C(string) for O(type=NAPTR). + type: int + returned: O(type=CAA) or O(type=DNSKEY) or O(type=NAPTR) or O(type=NSEC3) or O(type=NSEC3PARAM) + fp_type: + description: + - The fingerprint's type. + type: int + returned: O(type=SSHFP) + horizontal_precision: + description: + - The horizontal precision of the location. + type: float + returned: O(type=LOC) + inception: + description: + - The inception Unix timestamp. + type: int + returned: O(type=RRSIG) + iterations: + description: + - The number of iterations. + type: int + returned: O(type=NSEC3) or O(type=NSEC3PARAM) + key: + description: + - The key. + type: str + returned: O(type=DNSKEY) + key_tag: + description: + - The key's tag. + type: int + returned: O(type=DS) or O(type=RRSIG) + labels: + description: + - The labels. + type: int + returned: O(type=RRSIG) + latitude: + description: + - The location's latitude. + type: list + elements: int + returned: O(type=LOC) + longitude: + description: + - The location's longitude. + type: list + elements: int + returned: O(type=LOC) + mbox: + description: + - The mbox. + type: str + returned: O(type=RP) + minimum: + description: + - Used to calculate the TTL for purposes of negative caching. + type: int + returned: O(type=SOA) + mname: + description: + - Primary main name server for this zone. + type: str + returned: O(type=SOA) + mtype: + description: + - The mtype. + type: int + returned: O(type=TLSA) + next: + description: + - The next value. + type: str + returned: O(type=NSEC) or O(type=NSEC3) + order: + description: + - The order value. + type: int + returned: O(type=NAPTR) + original_ttl: + description: + - The original TTL. + type: int + returned: O(type=RRSIG) + os: + description: + - The operating system. + type: str + returned: O(type=HINFO) + port: + description: + - The port. + type: int + returned: O(type=SRV) + preference: + description: + - The preference value for this record. + type: int + returned: O(type=MX) or O(type=NAPTR) + priority: + description: + - The priority value for this record. + type: int + returned: O(type=SRV) + protocol: + description: + - The protocol. + type: int + returned: O(type=DNSKEY) + refresh: + description: + - Number of seconds after which secondary name servers should query the main + name server for the SOA record to detect zone changes. + type: int + returned: O(type=SOA) + regexp: + description: + - A regular expression. + type: str + returned: O(type=NAPTR) + replacement: + description: + - The replacement. + type: str + returned: O(type=NAPTR) + retry: + description: + - Number of seconds after which secondary name servers should retry to request + the serial number from the main name server if the main name server does not respond. + type: int + returned: O(type=SOA) + rname: + description: + - E-mail address of the administrator responsible for this zone. + type: str + returned: O(type=SOA) + salt: + description: + - The salt. + type: str + returned: O(type=NSEC3) or O(type=NSEC3PARAM) + selector: + description: + - The selector. + type: int + returned: O(type=TLSA) + serial: + description: + - Serial number for this zone. + type: int + returned: O(type=SOA) + service: + description: + - The service. + type: str + returned: O(type=NAPTR) + signature: + description: + - The signature. + type: str + returned: O(type=RRSIG) + signer: + description: + - The signer. + type: str + returned: O(type=RRSIG) + size: + description: + - The size of the location. + type: float + returned: O(type=LOC) + strings: + description: + - List of strings for this record. + - See RV(results[].result[].values[].value) for the concatenated result. + type: list + elements: str + returned: O(type=SPF) or O(type=TXT) + tag: + description: + - The tag. + type: str + returned: O(type=CAA) + target: + description: + - The target. + type: str + returned: O(type=CNAME) or O(type=DNAME) or O(type=NS) or O(type=PTR) or O(type=SRV) + txt: + description: + - The TXT value. + type: str + returned: O(type=RP) + type_covered: + description: + - The type covered. + type: str + returned: O(type=RRSIG) + usage: + description: + - The usage flag. + type: int + returned: O(type=TLSA) + value: + description: + - The value. + - For O(type=SPF) or O(type=TXT), this is the concatenation of RV(results[].result[].values[].strings). + type: str + returned: O(type=CAA) or O(type=SPF) or O(type=TXT) + vertical_precision: + description: + - The vertical precision of the location. + type: float + returned: O(type=LOC) + weight: + description: + - The service's weight. + type: int + returned: O(type=SRV) + windows: + description: + - The windows. + type: str + returned: O(type=NSEC) or O(type=NSEC3) + sample: + - name: www.example.com + result: + - nameserver: ns1.example.com + values: + - address: 127.0.0.1 + - nameserver: ns2.example.com + values: + - address: 127.0.0.1 + - name: example.org + result: + - nameserver: ns1.example.org + values: + - address: 127.0.0.1 + - address: 127.0.0.2 + - nameserver: ns2.example.org + values: + - address: 127.0.0.2 + - nameserver: ns3.example.org + values: + - address: 127.0.0.1 +''' + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.dns.plugins.module_utils.resolver import ( + ResolveDirectlyFromNameServers, + ResolverError, + assert_requirements_present, +) + +from ansible_collections.community.dns.plugins.module_utils.dnspython_records import ( + NAME_TO_RDTYPE, + convert_rdata_to_dict, +) + +try: + import dns.exception +except ImportError: + pass # handled in assert_requirements_present() + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True, type='list', elements='str'), + type=dict( + required=True, + type='str', + choices=[ + 'A', + 'ALL', + 'AAAA', + 'CAA', + 'CNAME', + 'DNAME', + 'DNSKEY', + 'DS', + 'HINFO', + 'LOC', + 'MX', + 'NAPTR', + 'NS', + 'NSEC', + 'NSEC3', + 'NSEC3PARAM', + 'PTR', + 'RP', + 'RRSIG', + 'SOA', + 'SPF', + 'SRV', + 'SSHFP', + 'TLSA', + 'TXT', + ], + ), + query_retry=dict(type='int', default=3), + query_timeout=dict(type='float', default=10), + always_ask_default_resolver=dict(type='bool', default=True), + ), + supports_check_mode=True, + ) + assert_requirements_present(module) + + names = module.params['name'] + record_type = module.params['type'] + + resolver = ResolveDirectlyFromNameServers( + timeout=module.params['query_timeout'], + timeout_retries=module.params['query_retry'], + always_ask_default_resolver=module.params['always_ask_default_resolver'], + ) + results = [None] * len(names) + for index, name in enumerate(names): + results[index] = { + 'name': name, + } + + rdtype = NAME_TO_RDTYPE[record_type] + + try: + for index, name in enumerate(names): + result = [] + results[index]['result'] = result + records_for_nameservers = resolver.resolve(name, rdtype=rdtype) + for nameserver, records in records_for_nameservers.items(): + ns_result = { + 'nameserver': nameserver, + } + result.append(ns_result) + values = [] + if records is not None: + for data in records: + values.append(convert_rdata_to_dict(data)) + ns_result['values'] = sorted(values) + result.sort(key=lambda v: v['nameserver']) + module.exit_json(results=results) + except ResolverError as e: + module.fail_json( + msg='Unexpected resolving error: {0}'.format(to_native(e)), + results=results, + exception=traceback.format_exc()) + except dns.exception.DNSException as e: + module.fail_json( + msg='Unexpected DNS error: {0}'.format(to_native(e)), + results=results, + exception=traceback.format_exc()) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/plugins/module_utils/test_dnspython_records.py b/tests/unit/plugins/module_utils/test_dnspython_records.py new file mode 100644 index 00000000..aa5808fa --- /dev/null +++ b/tests/unit/plugins/module_utils/test_dnspython_records.py @@ -0,0 +1,408 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022, Felix Fontein +# 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 + +# Make coding more python3-ish +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +import pytest + +from ansible_collections.community.dns.plugins.module_utils.dnspython_records import ( + RDTYPE_TO_FIELDS, + convert_rdata_to_dict, +) + +# We need dnspython +dns = pytest.importorskip('dns') + +import dns.version + + +TEST_CONVERT_RDATA_TO_DICT = [ + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'), + True, + False, + { + 'address': '3.3.3.3', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.AAAA, '1:2::3'), + True, + False, + { + 'address': '1:2::3', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.AAAA, '::'), + False, + True, + { + 'address': '::', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CAA, '10 issue letsencrypt.org'), + True, + False, + { + 'flags': 10, + 'tag': 'issue', + 'value': 'letsencrypt.org', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'foo.example.com.'), + True, + False, + { + 'target': 'foo.example.com.', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DNAME, 'foo.example.com.'), + True, + False, + { + 'target': 'foo.example.com.', + }, + ), + ( + dns.rdata.from_text( + dns.rdataclass.IN, + dns.rdatatype.DNSKEY, + '512 255 1 AQMFD5raczCJHViKtLYhWGz8hMY9UGRuniJDBzC7w0aR yzWZriO6i2odGWWQVucZqKVsENW91IOW4vqudngPZsY3' + ' GvQ/xVA8/7pyFj6b7Esga60zyGW6LFe9r8n6paHrlG5o jqf0BaqHT+8=', + ), + False, + False, + { + 'flags': 512, + 'algorithm': 1, + 'protocol': 255, + 'key': ( + 'AQMFD5raczCJHViKtLYhWGz8hMY9UGRuniJDBzC7w0aRyzWZriO6i2odGWWQVucZqKVsENW9' + '1IOW4vqudngPZsY3GvQ/xVA8/7pyFj6b7Esga60zyGW6LFe9r8n6paHrlG5ojqf0BaqHT+8=' + ), + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS, '12345 3 1 123456789abcdef67890123456789abcdef67890'), + False, + False, + { + 'algorithm': 3, + 'digest_type': 1, + 'key_tag': 12345, + 'digest': '123456789abcdef67890123456789abcdef67890', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.HINFO, '"Generic PC clone" "NetBSD-1.4"'), + False, + False, + { + 'cpu': b'Generic PC clone', + 'os': b'NetBSD-1.4', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.LOC, '60 9 0.000 N 24 39 0.000 E 10.00m 20.00m 2000.00m 20.00m'), + False, + False, + { + 'latitude': [60, 9, 0, 0, 1], + 'longitude': [24, 39, 0, 0, 1], + 'altitude': 1000.0, + 'size': 2000.0, + 'horizontal_precision': 200000.0, + 'vertical_precision': 2000.0, + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.MX, '10 mail.example.com'), + True, + False, + { + 'preference': 10, + 'exchange': 'mail.example.com', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NAPTR, '65535 65535 "text 1" "text 2" "text 3" example.com.'), + False, + False, + { + 'order': 65535, + 'preference': 65535, + 'flags': b'text 1', + 'service': b'text 2', + 'regexp': b'text 3', + 'replacement': 'example.com.', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.org.'), + True, + False, + { + 'target': 'ns.example.org.', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NSEC, 'a.secure A MX RRSIG NSEC TYPE1234'), + False, + False, + { + 'next': 'a.secure', + 'windows': 'A MX RRSIG NSEC TYPE1234', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NSEC3, '1 1 123 f00baa23 2t7b4g4vsa5smi47k61mv5bv1a22bojr NS SOA MX RRSIG DNSKEY NSEC3PARAM'), + False, + False, + { + 'algorithm': 1, + 'flags': 1, + 'iterations': 123, + 'salt': 'f00baa23', + 'next': '2t7b4g4vsa5smi47k61mv5bv1a22bojr', + 'windows': 'NS SOA MX RRSIG DNSKEY NSEC3PARAM', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NSEC3PARAM, '1 1 123 f00baa23'), + False, + False, + { + 'algorithm': 1, + 'flags': 1, + 'iterations': 123, + 'salt': 'f00baa23', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.PTR, 'example.com.'), + False, + False, + { + 'target': 'example.com.', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.RP, 'mbox-dname txt-dname'), + False, + False, + { + 'mbox': 'mbox-dname', + 'txt': 'txt-dname', + }, + ), + ( + dns.rdata.from_text( + dns.rdataclass.IN, + dns.rdatatype.RRSIG, + 'SOA 5 2 3600 20101127004331 20101119213831 61695 dnspython.org. sDUlltRlFTQw5ITFxOXW3TgmrHeMeNpdqcZ4EXxM9FHhIlte6V9YCnDw' + ' t6dvM9jAXdIEi03l9H/RAd9xNNW6gvGMHsBGzpvvqFQxIBR2PoiZA1mX /SWHZFdbt4xjYTtXqpyYvrMK0Dt7bUYPadyhPFCJ1B+I8Zi7B5WJEOd0 8vs=', + ), + False, + False, + { + 'type_covered': 'SOA', + 'algorithm': 5, + 'labels': 2, + 'original_ttl': 3600, + 'expiration': 1290818611, + 'inception': 1290202711, + 'key_tag': 61695, + 'signer': 'dnspython.org.', + 'signature': ( + 'sDUlltRlFTQw5ITFxOXW3TgmrHeMeNpdqcZ4EXxM9FHhIlte6V9YCnDwt6dvM9jAXdIEi03l9H/RAd9xNNW6gv' + 'GMHsBGzpvvqFQxIBR2PoiZA1mX/SWHZFdbt4xjYTtXqpyYvrMK0Dt7bUYPadyhPFCJ1B+I8Zi7B5WJEOd08vs=' + ), + }, + ), + ( + dns.rdata.from_text( + dns.rdataclass.IN, + dns.rdatatype.RRSIG, + 'NSEC 1 3 3600 20200101000000 20030101000000 2143 foo. MxFcby9k/yvedMfQgKzhH5er0Mu/vILz' + ' 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY=', + ), + False, + False, + { + 'type_covered': 'NSEC', + 'algorithm': 1, + 'labels': 3, + 'original_ttl': 3600, + 'expiration': 1577836800, + 'inception': 1041379200, + 'key_tag': 2143, + 'signer': 'foo.', + 'signature': 'MxFcby9k/yvedMfQgKzhH5er0Mu/vILz45IkskceFGgiWCn/GxHhai6VAuHAoNUz4YoU1tVfSCSqQYn6//11U6Nld80jEeC8aTrO+KKmCaY=', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.org. 1 7200 900 1209600 86400'), + False, + False, + { + 'mname': 'ns.example.com.', + 'rname': 'ns.example.org.', + 'serial': 1, + 'refresh': 7200, + 'retry': 900, + 'expire': 1209600, + 'minimum': 86400, + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SPF, '"v=spf1 a mx" " -all"'), + False, + False, + { + 'strings': [b'v=spf1 a mx', b' -all'], + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SPF, '"v=spf1 a mx" " -all"'), + False, + True, + { + 'strings': [b'v=spf1 a mx', b' -all'], + 'value': b'v=spf1 a mx -all', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SRV, r'0 1 443 exchange.example.com'), + False, + False, + { + 'priority': 0, + 'weight': 1, + 'port': 443, + 'target': 'exchange.example.com', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SSHFP, r'1 1 aa549bfe898489c02d1715d97d79c57ba2fa76ab'), + False, + False, + { + 'algorithm': 1, + 'fp_type': 1, + 'fingerprint': 'aa549bfe898489c02d1715d97d79c57ba2fa76ab', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TLSA, r'3 1 1 a9cdf989b504fe5dca90c0d2167b6550570734f7c763e09fdf88904e06157065'), + False, + False, + { + 'usage': 3, + 'selector': 1, + 'mtype': 1, + 'cert': 'a9cdf989b504fe5dca90c0d2167b6550570734f7c763e09fdf88904e06157065', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo bar"'), + False, + False, + { + 'strings': [b'asdf', b'foo bar'], + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo bar"'), + False, + True, + { + 'strings': [b'asdf', b'foo bar'], + 'value': b'asdffoo bar', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo bar"'), + True, + False, + { + 'strings': [u'asdf', u'foo bar'], + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo bar"'), + True, + True, + { + 'strings': [u'asdf', u'foo bar'], + 'value': u'asdffoo bar', + }, + ), +] + + +if dns.version.MAJOR >= 2: + # https://github.com/rthalley/dnspython/issues/321 makes this not working on dnspython < 2.0.0, + # which affects Python 3.5 and 2.x since these are only supported by dnspython < 2.0.0. + TEST_CONVERT_RDATA_TO_DICT.extend([ + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo \195\164"'), + False, + False, + { + 'strings': [b'asdf', b'foo \xC3\xA4'], + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo \195\164"'), + False, + True, + { + 'strings': [b'asdf', b'foo \xC3\xA4'], + 'value': b'asdffoo \xC3\xA4', + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo \195\164"'), + True, + False, + { + 'strings': [u'asdf', u'foo ä'], + }, + ), + ( + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo \195\164"'), + True, + True, + { + 'strings': [u'asdf', u'foo ä'], + 'value': u'asdffoo ä', + }, + ), + ]) + + +@pytest.mark.parametrize("rdata, to_unicode, add_synthetic, expected_result", TEST_CONVERT_RDATA_TO_DICT) +def test_convert_rdata_to_dict(rdata, to_unicode, add_synthetic, expected_result): + result = convert_rdata_to_dict(rdata, to_unicode=to_unicode, add_synthetic=add_synthetic) + print(expected_result) + print(result) + assert expected_result == result + + +def test_error(): + v = RDTYPE_TO_FIELDS.pop(dns.rdatatype.A) + with pytest.raises(ValueError) as exc: + convert_rdata_to_dict(dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3')) + RDTYPE_TO_FIELDS[dns.rdatatype.A] = v + print(exc.value.args) + assert exc.value.args == ('Unsupported record type 1', ) diff --git a/tests/unit/plugins/modules/test_nameserver_info.py b/tests/unit/plugins/modules/test_nameserver_info.py new file mode 100644 index 00000000..ce990f6d --- /dev/null +++ b/tests/unit/plugins/modules/test_nameserver_info.py @@ -0,0 +1,351 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022, Felix Fontein +# 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 + +# Make coding more python3-ish +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +import pytest + +from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock, patch + +from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import ( + set_module_args, + ModuleTestCase, + AnsibleExitJson, + AnsibleFailJson, +) + +from ansible_collections.community.dns.plugins.modules import nameserver_info + +from ..module_utils.resolver_helper import ( + mock_resolver, + mock_query_udp, + create_mock_answer, + create_mock_response, +) + +# We need dnspython +dns = pytest.importorskip('dns') + + +class TestNameserverInfo(ModuleTestCase): + def test_single(self): + fake_query = MagicMock() + fake_query.question = 'Doctor Who?' + resolver = mock_resolver(['1.1.1.1'], {}) + udp_sequence = [ + { + 'query_target': dns.name.from_unicode(u'com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'www.example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'www.example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'), + ), dns.rrset.from_rdata( + 'www.example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'example.org') + )]), + }, + ] + with patch('dns.resolver.get_default_resolver', resolver): + with patch('dns.resolver.Resolver', resolver): + with patch('dns.query.udp', mock_query_udp(udp_sequence)): + with pytest.raises(AnsibleExitJson) as exc: + set_module_args({ + 'name': ['www.example.com'], + }) + nameserver_info.main() + + print(exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert len(exc.value.args[0]['results']) == 1 + assert exc.value.args[0]['results'][0]['name'] == 'www.example.com' + assert exc.value.args[0]['results'][0]['nameservers'] == [ + 'ns.example.com', + ] + + def test_single_ips(self): + fake_query = MagicMock() + fake_query.question = 'Doctor Who?' + resolver = mock_resolver(['1.1.1.1'], { + ('1.1.1.1', ): [ + { + 'target': 'ns.example.com', + 'rdtype': dns.rdatatype.A, + 'lifetime': 10, + 'result': create_mock_answer(dns.rrset.from_rdata( + 'ns.example.com', + 300, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'), + )), + }, + { + 'target': 'ns.example.com', + 'rdtype': dns.rdatatype.AAAA, + 'lifetime': 10, + 'result': create_mock_answer(dns.rrset.from_rdata( + 'ns.example.com', + 300, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.AAAA, '1:2::3'), + )), + }, + ], + }) + udp_sequence = [ + { + 'query_target': dns.name.from_unicode(u'com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'www.example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'www.example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'), + ), dns.rrset.from_rdata( + 'www.example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'example.org') + )]), + }, + ] + with patch('dns.resolver.get_default_resolver', resolver): + with patch('dns.resolver.Resolver', resolver): + with patch('dns.query.udp', mock_query_udp(udp_sequence)): + with pytest.raises(AnsibleExitJson) as exc: + set_module_args({ + 'name': ['www.example.com'], + 'resolve_addresses': True, + }) + nameserver_info.main() + + print(exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert len(exc.value.args[0]['results']) == 1 + assert exc.value.args[0]['results'][0]['name'] == 'www.example.com' + assert exc.value.args[0]['results'][0]['nameservers'] == [ + '1:2::3', + '3.3.3.3', + ] + + def test_timeout(self): + fake_query = MagicMock() + fake_query.question = 'Doctor Who?' + resolver = mock_resolver(['1.1.1.1'], {}) + udp_sequence = [ + { + 'query_target': dns.name.from_unicode(u'com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 9, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 9, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'www.example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 9, + }, + 'raise': dns.exception.Timeout(timeout=9), + }, + { + 'query_target': dns.name.from_unicode(u'www.example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 9, + }, + 'result': create_mock_response(dns.rcode.NOERROR, authority=[dns.rrset.from_rdata( + 'www.example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'mail.example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 9, + }, + 'raise': dns.exception.Timeout(timeout=9), + }, + { + 'query_target': dns.name.from_unicode(u'mail.example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 9, + }, + 'raise': dns.exception.Timeout(timeout=9), + }, + ] + with patch('dns.resolver.get_default_resolver', resolver): + with patch('dns.resolver.Resolver', resolver): + with patch('dns.query.udp', mock_query_udp(udp_sequence)): + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({ + 'name': ['www.example.com', 'mail.example.com'], + 'query_timeout': 9, + 'query_retry': 1, + }) + nameserver_info.main() + + print(exc.value.args[0]) + assert exc.value.args[0]['msg'] in ( + 'Unexpected DNS error: The DNS operation timed out after 9 seconds', + 'Unexpected DNS error: The DNS operation timed out after 9.000 seconds', + ) + assert len(exc.value.args[0]['results']) == 2 + assert exc.value.args[0]['results'][0]['name'] == 'www.example.com' + assert exc.value.args[0]['results'][0]['nameservers'] == ['ns.example.com'] + assert exc.value.args[0]['results'][1]['name'] == 'mail.example.com' + assert 'nameservers' not in exc.value.args[0]['results'][1] + + def test_nxdomain(self): + resolver = mock_resolver(['1.1.1.1'], {}) + udp_sequence = [ + { + 'query_target': dns.name.from_unicode(u'com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NXDOMAIN), + }, + { + 'query_target': dns.name.from_unicode(u'com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NXDOMAIN), + }, + ] + with patch('dns.resolver.get_default_resolver', resolver): + with patch('dns.resolver.Resolver', resolver): + with patch('dns.query.udp', mock_query_udp(udp_sequence)): + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({ + 'name': ['www.example.com'], + }) + nameserver_info.main() + + print(exc.value.args[0]) + assert exc.value.args[0]['msg'] == 'Unexpected DNS error: The DNS query name does not exist: com.' + assert len(exc.value.args[0]['results']) == 1 + assert exc.value.args[0]['results'][0]['name'] == 'www.example.com' + assert 'nameservers' not in exc.value.args[0]['results'][0] + + def test_servfail(self): + resolver = mock_resolver(['1.1.1.1'], {}) + udp_sequence = [ + { + 'query_target': dns.name.from_unicode(u'com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.SERVFAIL), + }, + ] + with patch('dns.resolver.get_default_resolver', resolver): + with patch('dns.resolver.Resolver', resolver): + with patch('dns.query.udp', mock_query_udp(udp_sequence)): + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({ + 'name': ['www.example.com'], + }) + nameserver_info.main() + + print(exc.value.args[0]) + assert exc.value.args[0]['msg'] == 'Unexpected resolving error: Error SERVFAIL while querying 1.1.1.1 with query get NS for "com."' + assert len(exc.value.args[0]['results']) == 1 + assert exc.value.args[0]['results'][0]['name'] == 'www.example.com' + assert 'nameservers' not in exc.value.args[0]['results'][0] diff --git a/tests/unit/plugins/modules/test_nameserver_record_info.py b/tests/unit/plugins/modules/test_nameserver_record_info.py new file mode 100644 index 00000000..0a5dc9cb --- /dev/null +++ b/tests/unit/plugins/modules/test_nameserver_record_info.py @@ -0,0 +1,541 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022, Felix Fontein +# 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 + +# Make coding more python3-ish +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +import pytest + +from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import MagicMock, patch + +from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import ( + set_module_args, + ModuleTestCase, + AnsibleExitJson, + AnsibleFailJson, +) + +from ansible_collections.community.dns.plugins.modules import nameserver_record_info + +from ..module_utils.resolver_helper import ( + mock_resolver, + mock_query_udp, + create_mock_answer, + create_mock_response, +) + +# We need dnspython +dns = pytest.importorskip('dns') + + +class TestNameserverRecordInfo(ModuleTestCase): + def test_single(self): + fake_query = MagicMock() + fake_query.question = 'Doctor Who?' + resolver = mock_resolver(['1.1.1.1'], { + ('1.1.1.1', ): [ + { + 'target': 'ns.example.com', + 'rdtype': dns.rdatatype.A, + 'lifetime': 10, + 'result': create_mock_answer(dns.rrset.from_rdata( + 'ns.example.com', + 300, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'), + )), + }, + { + 'target': 'ns.example.com', + 'rdtype': dns.rdatatype.AAAA, + 'lifetime': 10, + 'result': create_mock_answer(dns.rrset.from_rdata( + 'ns.example.com', + 300, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.AAAA, '1:2::3'), + )), + }, + { + 'target': 'ns.example.org', + 'rdtype': dns.rdatatype.A, + 'lifetime': 10, + 'result': create_mock_answer(dns.rrset.from_rdata( + 'ns.example.org', + 300, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '4.4.4.4'), + )), + }, + { + 'target': 'ns.example.org', + 'rdtype': dns.rdatatype.AAAA, + 'lifetime': 10, + 'raise': dns.resolver.NoAnswer(response=fake_query), + }, + ], + ('1:2::3', '3.3.3.3'): [ + { + 'target': dns.name.from_unicode(u'example.org'), + 'rdtype': dns.rdatatype.TXT, + 'lifetime': 10, + 'result': create_mock_answer(dns.rrset.from_rdata( + 'example.org', + 300, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'asdf'), + )), + }, + ], + ('4.4.4.4', ): [ + { + 'target': dns.name.from_unicode(u'example.org'), + 'rdtype': dns.rdatatype.TXT, + 'lifetime': 10, + 'result': create_mock_answer(dns.rrset.from_rdata( + 'example.org', + 300, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'asdf'), + )), + }, + ], + }) + udp_sequence = [ + { + 'query_target': dns.name.from_unicode(u'com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'www.example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'www.example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'), + ), dns.rrset.from_rdata( + 'www.example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'example.org') + )]), + }, + { + 'query_target': dns.name.from_unicode(u'org'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'org', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.org'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'example.org'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'example.org', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.org'), + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'), + )]), + }, + ] + with patch('dns.resolver.get_default_resolver', resolver): + with patch('dns.resolver.Resolver', resolver): + with patch('dns.query.udp', mock_query_udp(udp_sequence)): + with pytest.raises(AnsibleExitJson) as exc: + set_module_args({ + 'name': ['www.example.com'], + 'type': 'TXT', + }) + nameserver_record_info.main() + + print(exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert len(exc.value.args[0]['results']) == 1 + assert exc.value.args[0]['results'][0]['name'] == 'www.example.com' + assert len(exc.value.args[0]['results'][0]['result']) == 2 + assert exc.value.args[0]['results'][0]['result'][0]['nameserver'] == 'ns.example.com' + assert exc.value.args[0]['results'][0]['result'][0]['values'] == [ + { + 'strings': ['asdf'], + 'value': 'asdf', + } + ] + assert exc.value.args[0]['results'][0]['result'][1]['nameserver'] == 'ns.example.org' + assert exc.value.args[0]['results'][0]['result'][1]['values'] == [ + { + 'strings': ['asdf'], + 'value': 'asdf', + } + ] + + def test_timeout(self): + fake_query = MagicMock() + fake_query.question = 'Doctor Who?' + resolver = mock_resolver(['1.1.1.1'], { + ('1.1.1.1', ): [ + { + 'target': 'ns.example.com', + 'rdtype': dns.rdatatype.A, + 'lifetime': 9, + 'result': create_mock_answer(dns.rrset.from_rdata( + 'ns.example.com', + 300, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'), + )), + }, + { + 'target': 'ns.example.com', + 'rdtype': dns.rdatatype.AAAA, + 'lifetime': 9, + 'raise': dns.resolver.NoAnswer(response=fake_query), + }, + ], + ('3.3.3.3', ): [ + { + 'target': dns.name.from_unicode(u'www.example.com'), + 'rdtype': dns.rdatatype.TXT, + 'lifetime': 9, + 'raise': dns.exception.Timeout(timeout=9), + }, + { + 'target': dns.name.from_unicode(u'www.example.com'), + 'rdtype': dns.rdatatype.TXT, + 'lifetime': 9, + 'result': create_mock_answer(dns.rrset.from_rdata( + 'www.example.com', + 300, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, 'fdsa asdf'), + )), + }, + { + 'target': dns.name.from_unicode(u'mail.example.com'), + 'rdtype': dns.rdatatype.TXT, + 'lifetime': 9, + 'raise': dns.exception.Timeout(timeout=9), + }, + { + 'target': dns.name.from_unicode(u'mail.example.com'), + 'rdtype': dns.rdatatype.TXT, + 'lifetime': 9, + 'raise': dns.exception.Timeout(timeout=9), + }, + ], + }) + udp_sequence = [ + { + 'query_target': dns.name.from_unicode(u'com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 9, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 9, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'www.example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 9, + }, + 'result': create_mock_response(dns.rcode.NOERROR, authority=[dns.rrset.from_rdata( + 'www.example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'mail.example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 9, + }, + 'result': create_mock_response(dns.rcode.NOERROR, authority=[dns.rrset.from_rdata( + 'mail.example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'), + )]), + }, + ] + with patch('dns.resolver.get_default_resolver', resolver): + with patch('dns.resolver.Resolver', resolver): + with patch('dns.query.udp', mock_query_udp(udp_sequence)): + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({ + 'name': ['www.example.com', 'mail.example.com'], + 'type': 'TXT', + 'query_timeout': 9, + 'query_retry': 1, + }) + nameserver_record_info.main() + + print(exc.value.args[0]) + assert exc.value.args[0]['msg'] in ( + 'Unexpected DNS error: The DNS operation timed out after 9 seconds', + 'Unexpected DNS error: The DNS operation timed out after 9.000 seconds', + ) + assert len(exc.value.args[0]['results']) == 2 + assert exc.value.args[0]['results'][0]['name'] == 'www.example.com' + assert exc.value.args[0]['results'][0]['result'] == [ + { + 'nameserver': 'ns.example.com', + 'values': [ + { + 'strings': ['fdsa', 'asdf'], + 'value': 'fdsaasdf', + } + ], + } + ] + assert exc.value.args[0]['results'][1]['name'] == 'mail.example.com' + assert exc.value.args[0]['results'][1]['result'] == [] + + def test_nxdomain(self): + resolver = mock_resolver(['1.1.1.1'], {}) + udp_sequence = [ + { + 'query_target': dns.name.from_unicode(u'com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NXDOMAIN), + }, + { + 'query_target': dns.name.from_unicode(u'com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NXDOMAIN), + }, + ] + with patch('dns.resolver.get_default_resolver', resolver): + with patch('dns.resolver.Resolver', resolver): + with patch('dns.query.udp', mock_query_udp(udp_sequence)): + with pytest.raises(AnsibleExitJson) as exc: + set_module_args({ + 'name': ['www.example.com'], + 'type': 'TXT', + }) + nameserver_record_info.main() + + print(exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert len(exc.value.args[0]['results']) == 1 + assert exc.value.args[0]['results'][0]['name'] == 'www.example.com' + assert exc.value.args[0]['results'][0]['result'] == [] + + def test_servfail(self): + resolver = mock_resolver(['1.1.1.1'], {}) + udp_sequence = [ + { + 'query_target': dns.name.from_unicode(u'com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.SERVFAIL), + }, + ] + with patch('dns.resolver.get_default_resolver', resolver): + with patch('dns.resolver.Resolver', resolver): + with patch('dns.query.udp', mock_query_udp(udp_sequence)): + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({ + 'name': ['www.example.com'], + 'type': 'TXT', + }) + nameserver_record_info.main() + + print(exc.value.args[0]) + assert exc.value.args[0]['msg'] == 'Unexpected resolving error: Error SERVFAIL while querying 1.1.1.1 with query get NS for "com."' + assert len(exc.value.args[0]['results']) == 1 + assert exc.value.args[0]['results'][0]['name'] == 'www.example.com' + assert exc.value.args[0]['results'][0]['result'] == [] + + def test_cname_loop(self): + fake_query = MagicMock() + fake_query.question = 'Doctor Who?' + resolver = mock_resolver(['1.1.1.1'], { + ('1.1.1.1', ): [ + { + 'target': 'ns.example.com', + 'rdtype': dns.rdatatype.A, + 'lifetime': 10, + 'result': create_mock_answer(dns.rrset.from_rdata( + 'ns.example.com', + 300, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'), + )), + }, + { + 'target': 'ns.example.com', + 'rdtype': dns.rdatatype.AAAA, + 'lifetime': 10, + 'raise': dns.resolver.NoAnswer(response=fake_query), + }, + { + 'target': 'ns.example.org', + 'rdtype': dns.rdatatype.A, + 'lifetime': 10, + 'result': create_mock_answer(dns.rrset.from_rdata( + 'ns.example.org', + 300, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '4.4.4.4'), + )), + }, + { + 'target': 'ns.example.org', + 'rdtype': dns.rdatatype.AAAA, + 'lifetime': 10, + 'raise': dns.resolver.NoAnswer(response=fake_query), + }, + ], + }) + udp_sequence = [ + { + 'query_target': dns.name.from_unicode(u'com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.com'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.com'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'www.example.com'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'www.example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.com. 12345 7200 120 2419200 10800'), + ), dns.rrset.from_rdata( + 'www.example.com', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'example.org') + )]), + }, + { + 'query_target': dns.name.from_unicode(u'org'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'org', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.org'), + )]), + }, + { + 'query_target': dns.name.from_unicode(u'example.org'), + 'query_type': dns.rdatatype.NS, + 'nameserver': '1.1.1.1', + 'kwargs': { + 'timeout': 10, + }, + 'result': create_mock_response(dns.rcode.NOERROR, answer=[dns.rrset.from_rdata( + 'example.org', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.org'), + ), dns.rrset.from_rdata( + 'example.org', + 3600, + dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'www.example.com') + )]), + }, + ] + with patch('dns.resolver.get_default_resolver', resolver): + with patch('dns.resolver.Resolver', resolver): + with patch('dns.query.udp', mock_query_udp(udp_sequence)): + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({ + 'name': ['www.example.com'], + 'type': 'TXT', + }) + nameserver_record_info.main() + + print(exc.value.args[0]) + assert exc.value.args[0]['msg'] == 'Unexpected resolving error: Found CNAME loop starting at www.example.com' + assert len(exc.value.args[0]['results']) == 1 + assert exc.value.args[0]['results'][0]['name'] == 'www.example.com' + assert exc.value.args[0]['results'][0]['result'] == [] From fa454610a8c1a2698cf0125f68fc570ee7ae3221 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 28 Jul 2023 21:45:29 +0200 Subject: [PATCH 2/4] Improve tests. --- .../module_utils/test_dnspython_records.py | 116 +++++++----------- 1 file changed, 42 insertions(+), 74 deletions(-) diff --git a/tests/unit/plugins/module_utils/test_dnspython_records.py b/tests/unit/plugins/module_utils/test_dnspython_records.py index aa5808fa..9594b809 100644 --- a/tests/unit/plugins/module_utils/test_dnspython_records.py +++ b/tests/unit/plugins/module_utils/test_dnspython_records.py @@ -25,32 +25,28 @@ TEST_CONVERT_RDATA_TO_DICT = [ ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3'), - True, - False, + {'to_unicode': True, 'add_synthetic': False}, { 'address': '3.3.3.3', }, ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.AAAA, '1:2::3'), - True, - False, + {'to_unicode': True, 'add_synthetic': False}, { 'address': '1:2::3', }, ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.AAAA, '::'), - False, - True, + {'to_unicode': False, 'add_synthetic': True}, { 'address': '::', }, ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CAA, '10 issue letsencrypt.org'), - True, - False, + {'to_unicode': True, 'add_synthetic': False}, { 'flags': 10, 'tag': 'issue', @@ -59,16 +55,14 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.CNAME, 'foo.example.com.'), - True, - False, + {'to_unicode': True, 'add_synthetic': False}, { 'target': 'foo.example.com.', }, ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DNAME, 'foo.example.com.'), - True, - False, + {'to_unicode': True, 'add_synthetic': False}, { 'target': 'foo.example.com.', }, @@ -80,8 +74,7 @@ '512 255 1 AQMFD5raczCJHViKtLYhWGz8hMY9UGRuniJDBzC7w0aR yzWZriO6i2odGWWQVucZqKVsENW91IOW4vqudngPZsY3' ' GvQ/xVA8/7pyFj6b7Esga60zyGW6LFe9r8n6paHrlG5o jqf0BaqHT+8=', ), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'flags': 512, 'algorithm': 1, @@ -94,8 +87,7 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS, '12345 3 1 123456789abcdef67890123456789abcdef67890'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'algorithm': 3, 'digest_type': 1, @@ -105,8 +97,7 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.HINFO, '"Generic PC clone" "NetBSD-1.4"'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'cpu': b'Generic PC clone', 'os': b'NetBSD-1.4', @@ -114,8 +105,7 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.LOC, '60 9 0.000 N 24 39 0.000 E 10.00m 20.00m 2000.00m 20.00m'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'latitude': [60, 9, 0, 0, 1], 'longitude': [24, 39, 0, 0, 1], @@ -127,8 +117,7 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.MX, '10 mail.example.com'), - True, - False, + {'to_unicode': True, 'add_synthetic': False}, { 'preference': 10, 'exchange': 'mail.example.com', @@ -136,8 +125,7 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NAPTR, '65535 65535 "text 1" "text 2" "text 3" example.com.'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'order': 65535, 'preference': 65535, @@ -149,16 +137,14 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, 'ns.example.org.'), - True, - False, + {'to_unicode': True, 'add_synthetic': False}, { 'target': 'ns.example.org.', }, ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NSEC, 'a.secure A MX RRSIG NSEC TYPE1234'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'next': 'a.secure', 'windows': 'A MX RRSIG NSEC TYPE1234', @@ -166,8 +152,7 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NSEC3, '1 1 123 f00baa23 2t7b4g4vsa5smi47k61mv5bv1a22bojr NS SOA MX RRSIG DNSKEY NSEC3PARAM'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'algorithm': 1, 'flags': 1, @@ -179,8 +164,7 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NSEC3PARAM, '1 1 123 f00baa23'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'algorithm': 1, 'flags': 1, @@ -190,16 +174,14 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.PTR, 'example.com.'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'target': 'example.com.', }, ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.RP, 'mbox-dname txt-dname'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'mbox': 'mbox-dname', 'txt': 'txt-dname', @@ -212,8 +194,7 @@ 'SOA 5 2 3600 20101127004331 20101119213831 61695 dnspython.org. sDUlltRlFTQw5ITFxOXW3TgmrHeMeNpdqcZ4EXxM9FHhIlte6V9YCnDw' ' t6dvM9jAXdIEi03l9H/RAd9xNNW6gvGMHsBGzpvvqFQxIBR2PoiZA1mX /SWHZFdbt4xjYTtXqpyYvrMK0Dt7bUYPadyhPFCJ1B+I8Zi7B5WJEOd0 8vs=', ), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'type_covered': 'SOA', 'algorithm': 5, @@ -236,8 +217,7 @@ 'NSEC 1 3 3600 20200101000000 20030101000000 2143 foo. MxFcby9k/yvedMfQgKzhH5er0Mu/vILz' ' 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY=', ), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'type_covered': 'NSEC', 'algorithm': 1, @@ -252,8 +232,7 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.example.com. ns.example.org. 1 7200 900 1209600 86400'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'mname': 'ns.example.com.', 'rname': 'ns.example.org.', @@ -266,16 +245,14 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SPF, '"v=spf1 a mx" " -all"'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'strings': [b'v=spf1 a mx', b' -all'], }, ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SPF, '"v=spf1 a mx" " -all"'), - False, - True, + {'to_unicode': False, 'add_synthetic': True}, { 'strings': [b'v=spf1 a mx', b' -all'], 'value': b'v=spf1 a mx -all', @@ -283,8 +260,7 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SRV, r'0 1 443 exchange.example.com'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'priority': 0, 'weight': 1, @@ -294,8 +270,7 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.SSHFP, r'1 1 aa549bfe898489c02d1715d97d79c57ba2fa76ab'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'algorithm': 1, 'fp_type': 1, @@ -304,8 +279,7 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TLSA, r'3 1 1 a9cdf989b504fe5dca90c0d2167b6550570734f7c763e09fdf88904e06157065'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'usage': 3, 'selector': 1, @@ -315,16 +289,14 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo bar"'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'strings': [b'asdf', b'foo bar'], }, ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo bar"'), - False, - True, + {'to_unicode': False, 'add_synthetic': True}, { 'strings': [b'asdf', b'foo bar'], 'value': b'asdffoo bar', @@ -332,16 +304,14 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo bar"'), - True, - False, + {'to_unicode': True, 'add_synthetic': False}, { 'strings': [u'asdf', u'foo bar'], }, ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo bar"'), - True, - True, + {'to_unicode': True, 'add_synthetic': True}, { 'strings': [u'asdf', u'foo bar'], 'value': u'asdffoo bar', @@ -356,16 +326,14 @@ TEST_CONVERT_RDATA_TO_DICT.extend([ ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo \195\164"'), - False, - False, + {'to_unicode': False, 'add_synthetic': False}, { 'strings': [b'asdf', b'foo \xC3\xA4'], }, ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo \195\164"'), - False, - True, + {'to_unicode': False, 'add_synthetic': True}, { 'strings': [b'asdf', b'foo \xC3\xA4'], 'value': b'asdffoo \xC3\xA4', @@ -373,16 +341,14 @@ ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo \195\164"'), - True, - False, + {'to_unicode': True, 'add_synthetic': False}, { 'strings': [u'asdf', u'foo ä'], }, ), ( dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, r'asdf "foo \195\164"'), - True, - True, + {'to_unicode': True, 'add_synthetic': True}, { 'strings': [u'asdf', u'foo ä'], 'value': u'asdffoo ä', @@ -391,9 +357,9 @@ ]) -@pytest.mark.parametrize("rdata, to_unicode, add_synthetic, expected_result", TEST_CONVERT_RDATA_TO_DICT) -def test_convert_rdata_to_dict(rdata, to_unicode, add_synthetic, expected_result): - result = convert_rdata_to_dict(rdata, to_unicode=to_unicode, add_synthetic=add_synthetic) +@pytest.mark.parametrize("rdata, kwarg, expected_result", TEST_CONVERT_RDATA_TO_DICT) +def test_convert_rdata_to_dict(rdata, kwarg, expected_result): + result = convert_rdata_to_dict(rdata, **kwarg) print(expected_result) print(result) assert expected_result == result @@ -401,8 +367,10 @@ def test_convert_rdata_to_dict(rdata, to_unicode, add_synthetic, expected_result def test_error(): v = RDTYPE_TO_FIELDS.pop(dns.rdatatype.A) - with pytest.raises(ValueError) as exc: - convert_rdata_to_dict(dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3')) - RDTYPE_TO_FIELDS[dns.rdatatype.A] = v + try: + with pytest.raises(ValueError) as exc: + convert_rdata_to_dict(dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.A, '3.3.3.3')) + finally: + RDTYPE_TO_FIELDS[dns.rdatatype.A] = v print(exc.value.args) assert exc.value.args == ('Unsupported record type 1', ) From cff3c142871407bc3b144bd823a912eab9939df4 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 28 Jul 2023 21:47:25 +0200 Subject: [PATCH 3/4] Improve formulation. --- plugins/modules/nameserver_info.py | 2 +- plugins/modules/nameserver_record_info.py | 2 +- plugins/modules/wait_for_txt.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/nameserver_info.py b/plugins/modules/nameserver_info.py index 3ed08888..1b7f6394 100644 --- a/plugins/modules/nameserver_info.py +++ b/plugins/modules/nameserver_info.py @@ -49,7 +49,7 @@ of a subzone. - When set to V(false), will use the authoritative nameservers of the parent zone to find the authoritative nameservers of a subzone. This only makes sense when the nameservers were recently - changed and haven't propagated. + changed and have not yet propagated. type: bool default: true requirements: diff --git a/plugins/modules/nameserver_record_info.py b/plugins/modules/nameserver_record_info.py index ced82a27..37f10e6f 100644 --- a/plugins/modules/nameserver_record_info.py +++ b/plugins/modules/nameserver_record_info.py @@ -76,7 +76,7 @@ of a subzone. - When set to V(false), will use the authoritative nameservers of the parent zone to find the authoritative nameservers of a subzone. This only makes sense when the nameservers were recently - changed and haven't propagated. + changed and have not yet propagated. type: bool default: true requirements: diff --git a/plugins/modules/wait_for_txt.py b/plugins/modules/wait_for_txt.py index 5378d20c..d99e0a39 100644 --- a/plugins/modules/wait_for_txt.py +++ b/plugins/modules/wait_for_txt.py @@ -97,7 +97,7 @@ of a subzone. - When set to V(false), will use the authoritative nameservers of the parent zone to find the authoritative nameservers of a subzone. This only makes sense when the nameservers were recently - changed and haven't propagated. + changed and have not yet propagated. type: bool default: true requirements: From dd8279c9b666a935361a36040f8632a051c24813 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 31 Jul 2023 16:52:33 +0200 Subject: [PATCH 4/4] Improve return value docs. --- plugins/modules/nameserver_record_info.py | 112 +++++++++++----------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/plugins/modules/nameserver_record_info.py b/plugins/modules/nameserver_record_info.py index 37f10e6f..9dd787d2 100644 --- a/plugins/modules/nameserver_record_info.py +++ b/plugins/modules/nameserver_record_info.py @@ -140,7 +140,7 @@ description: - The records of type O(type). - Depending on O(type), different fields are returned. - - For O(type=TXT) and O(type=SPF), also the concatenated value is returned as C(values). + - For O(type=TXT) and O(type=SPF), also the concatenated value is returned as RV(results[].result[].values[].value). returned: success type: list elements: dict @@ -151,286 +151,286 @@ description: - A IPv4 respectively IPv6 address. type: str - returned: O(type=A) or O(type=AAAA) + returned: if O(type=A) or O(type=AAAA) algorithm: description: - The algorithm ID. type: int - returned: O(type=DNSKEY) or O(type=DS) or O(type=NSEC3) or O(type=NSEC3PARAM) or O(type=RRSIG) or O(type=SSHFP) + returned: if O(type=DNSKEY) or O(type=DS) or O(type=NSEC3) or O(type=NSEC3PARAM) or O(type=RRSIG) or O(type=SSHFP) altitude: description: - The altitute. type: float - returned: O(type=LOC) + returned: if O(type=LOC) cert: description: - The certificate. type: str - returned: O(type=TLSA) + returned: if O(type=TLSA) cpu: description: - The CPU. type: str - returned: O(type=HINFO) + returned: if O(type=HINFO) digest: description: - The digest. type: str - returned: O(type=DS) + returned: if O(type=DS) digest_type: description: - The digest's type. type: int - returned: O(type=DS) + returned: if O(type=DS) exchange: description: - The exchange server. type: str - returned: O(type=MX) + returned: if O(type=MX) expiration: description: - The expiration Unix timestamp. type: int - returned: O(type=RRSIG) + returned: if O(type=RRSIG) expire: description: - Number of seconds after which secondary name servers should stop answering request for this zone if the main name server does not respond. type: int - returned: O(type=SOA) + returned: if O(type=SOA) fingerprint: description: - The fingerprint. type: str - returned: O(type=SSHFP) + returned: if O(type=SSHFP) flags: description: - Flags. - This is actually of type C(string) for O(type=NAPTR). type: int - returned: O(type=CAA) or O(type=DNSKEY) or O(type=NAPTR) or O(type=NSEC3) or O(type=NSEC3PARAM) + returned: if O(type=CAA) or O(type=DNSKEY) or O(type=NAPTR) or O(type=NSEC3) or O(type=NSEC3PARAM) fp_type: description: - The fingerprint's type. type: int - returned: O(type=SSHFP) + returned: if O(type=SSHFP) horizontal_precision: description: - The horizontal precision of the location. type: float - returned: O(type=LOC) + returned: if O(type=LOC) inception: description: - The inception Unix timestamp. type: int - returned: O(type=RRSIG) + returned: if O(type=RRSIG) iterations: description: - The number of iterations. type: int - returned: O(type=NSEC3) or O(type=NSEC3PARAM) + returned: if O(type=NSEC3) or O(type=NSEC3PARAM) key: description: - The key. type: str - returned: O(type=DNSKEY) + returned: if O(type=DNSKEY) key_tag: description: - The key's tag. type: int - returned: O(type=DS) or O(type=RRSIG) + returned: if O(type=DS) or O(type=RRSIG) labels: description: - The labels. type: int - returned: O(type=RRSIG) + returned: if O(type=RRSIG) latitude: description: - The location's latitude. type: list elements: int - returned: O(type=LOC) + returned: if O(type=LOC) longitude: description: - The location's longitude. type: list elements: int - returned: O(type=LOC) + returned: if O(type=LOC) mbox: description: - The mbox. type: str - returned: O(type=RP) + returned: if O(type=RP) minimum: description: - Used to calculate the TTL for purposes of negative caching. type: int - returned: O(type=SOA) + returned: if O(type=SOA) mname: description: - Primary main name server for this zone. type: str - returned: O(type=SOA) + returned: if O(type=SOA) mtype: description: - The mtype. type: int - returned: O(type=TLSA) + returned: if O(type=TLSA) next: description: - The next value. type: str - returned: O(type=NSEC) or O(type=NSEC3) + returned: if O(type=NSEC) or O(type=NSEC3) order: description: - The order value. type: int - returned: O(type=NAPTR) + returned: if O(type=NAPTR) original_ttl: description: - The original TTL. type: int - returned: O(type=RRSIG) + returned: if O(type=RRSIG) os: description: - The operating system. type: str - returned: O(type=HINFO) + returned: if O(type=HINFO) port: description: - The port. type: int - returned: O(type=SRV) + returned: if O(type=SRV) preference: description: - The preference value for this record. type: int - returned: O(type=MX) or O(type=NAPTR) + returned: if O(type=MX) or O(type=NAPTR) priority: description: - The priority value for this record. type: int - returned: O(type=SRV) + returned: if O(type=SRV) protocol: description: - The protocol. type: int - returned: O(type=DNSKEY) + returned: if O(type=DNSKEY) refresh: description: - Number of seconds after which secondary name servers should query the main name server for the SOA record to detect zone changes. type: int - returned: O(type=SOA) + returned: if O(type=SOA) regexp: description: - A regular expression. type: str - returned: O(type=NAPTR) + returned: if O(type=NAPTR) replacement: description: - The replacement. type: str - returned: O(type=NAPTR) + returned: if O(type=NAPTR) retry: description: - Number of seconds after which secondary name servers should retry to request the serial number from the main name server if the main name server does not respond. type: int - returned: O(type=SOA) + returned: if O(type=SOA) rname: description: - E-mail address of the administrator responsible for this zone. type: str - returned: O(type=SOA) + returned: if O(type=SOA) salt: description: - The salt. type: str - returned: O(type=NSEC3) or O(type=NSEC3PARAM) + returned: if O(type=NSEC3) or O(type=NSEC3PARAM) selector: description: - The selector. type: int - returned: O(type=TLSA) + returned: if O(type=TLSA) serial: description: - Serial number for this zone. type: int - returned: O(type=SOA) + returned: if O(type=SOA) service: description: - The service. type: str - returned: O(type=NAPTR) + returned: if O(type=NAPTR) signature: description: - The signature. type: str - returned: O(type=RRSIG) + returned: if O(type=RRSIG) signer: description: - The signer. type: str - returned: O(type=RRSIG) + returned: if O(type=RRSIG) size: description: - The size of the location. type: float - returned: O(type=LOC) + returned: if O(type=LOC) strings: description: - List of strings for this record. - See RV(results[].result[].values[].value) for the concatenated result. type: list elements: str - returned: O(type=SPF) or O(type=TXT) + returned: if O(type=SPF) or O(type=TXT) tag: description: - The tag. type: str - returned: O(type=CAA) + returned: if O(type=CAA) target: description: - The target. type: str - returned: O(type=CNAME) or O(type=DNAME) or O(type=NS) or O(type=PTR) or O(type=SRV) + returned: if O(type=CNAME) or O(type=DNAME) or O(type=NS) or O(type=PTR) or O(type=SRV) txt: description: - The TXT value. type: str - returned: O(type=RP) + returned: if O(type=RP) type_covered: description: - The type covered. type: str - returned: O(type=RRSIG) + returned: if O(type=RRSIG) usage: description: - The usage flag. type: int - returned: O(type=TLSA) + returned: if O(type=TLSA) value: description: - The value. - For O(type=SPF) or O(type=TXT), this is the concatenation of RV(results[].result[].values[].strings). type: str - returned: O(type=CAA) or O(type=SPF) or O(type=TXT) + returned: if O(type=CAA) or O(type=SPF) or O(type=TXT) vertical_precision: description: - The vertical precision of the location. type: float - returned: O(type=LOC) + returned: if O(type=LOC) weight: description: - The service's weight. type: int - returned: O(type=SRV) + returned: if O(type=SRV) windows: description: - The windows. type: str - returned: O(type=NSEC) or O(type=NSEC3) + returned: if O(type=NSEC) or O(type=NSEC3) sample: - name: www.example.com result: