Skip to content

Commit

Permalink
Add nameserver_info and nameserver_record_info modules.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfontein committed Jul 27, 2023
1 parent 0235d9c commit 344c234
Show file tree
Hide file tree
Showing 6 changed files with 2,172 additions and 0 deletions.
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 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, to_text

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

0 comments on commit 344c234

Please sign in to comment.