Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add nameserver_info and nameserver_record_info modules #133

Merged
merged 4 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions plugins/module_utils/dnspython_records.py
Original file line number Diff line number Diff line change
@@ -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
169 changes: 169 additions & 0 deletions plugins/modules/nameserver_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# 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


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 have not yet 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()
Loading