2015-08-21 04:22:01 +00:00
|
|
|
# (c) 2015, Jan-Piet Mens <jpmens(at)gmail.com>
|
|
|
|
#
|
|
|
|
# This file is part of Ansible
|
|
|
|
#
|
|
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from __future__ import (absolute_import, division, print_function)
|
|
|
|
__metaclass__ = type
|
|
|
|
|
|
|
|
from ansible.errors import AnsibleError
|
|
|
|
from ansible.plugins.lookup import LookupBase
|
2017-05-19 16:56:55 +00:00
|
|
|
from ansible.module_utils._text import to_native
|
2015-08-21 04:22:01 +00:00
|
|
|
import socket
|
|
|
|
|
|
|
|
try:
|
2017-03-26 16:24:30 +00:00
|
|
|
import dns.exception
|
|
|
|
import dns.name
|
2015-08-21 04:22:01 +00:00
|
|
|
import dns.resolver
|
|
|
|
import dns.reversename
|
2017-04-04 19:00:56 +00:00
|
|
|
import dns.rdataclass
|
2015-10-19 18:58:36 +00:00
|
|
|
from dns.rdatatype import (A, AAAA, CNAME, DLV, DNAME, DNSKEY, DS, HINFO, LOC,
|
2017-06-02 11:14:11 +00:00
|
|
|
MX, NAPTR, NS, NSEC3PARAM, PTR, RP, SOA, SPF, SRV, SSHFP, TLSA, TXT)
|
2015-08-21 04:22:01 +00:00
|
|
|
HAVE_DNS = True
|
|
|
|
except ImportError:
|
|
|
|
HAVE_DNS = False
|
|
|
|
|
2017-06-02 11:14:11 +00:00
|
|
|
|
2015-08-21 04:22:01 +00:00
|
|
|
def make_rdata_dict(rdata):
|
|
|
|
''' While the 'dig' lookup plugin supports anything which dnspython supports
|
|
|
|
out of the box, the following supported_types list describes which
|
|
|
|
DNS query types we can convert to a dict.
|
|
|
|
|
|
|
|
Note: adding support for RRSIG is hard work. :)
|
|
|
|
'''
|
|
|
|
supported_types = {
|
2017-06-02 11:14:11 +00:00
|
|
|
A: ['address'],
|
|
|
|
AAAA: ['address'],
|
|
|
|
CNAME: ['target'],
|
|
|
|
DNAME: ['target'],
|
|
|
|
DLV: ['algorithm', 'digest_type', 'key_tag', 'digest'],
|
|
|
|
DNSKEY: ['flags', 'algorithm', 'protocol', 'key'],
|
|
|
|
DS: ['algorithm', 'digest_type', 'key_tag', 'digest'],
|
|
|
|
HINFO: ['cpu', 'os'],
|
|
|
|
LOC: ['latitude', 'longitude', 'altitude', 'size', 'horizontal_precision', 'vertical_precision'],
|
|
|
|
MX: ['preference', 'exchange'],
|
|
|
|
NAPTR: ['order', 'preference', 'flags', 'service', 'regexp', 'replacement'],
|
|
|
|
NS: ['target'],
|
|
|
|
NSEC3PARAM: ['algorithm', 'flags', 'iterations', 'salt'],
|
|
|
|
PTR: ['target'],
|
|
|
|
RP: ['mbox', 'txt'],
|
|
|
|
# RRSIG: ['algorithm', 'labels', 'original_ttl', 'expiration', 'inception', 'signature'],
|
|
|
|
SOA: ['mname', 'rname', 'serial', 'refresh', 'retry', 'expire', 'minimum'],
|
|
|
|
SPF: ['strings'],
|
|
|
|
SRV: ['priority', 'weight', 'port', 'target'],
|
|
|
|
SSHFP: ['algorithm', 'fp_type', 'fingerprint'],
|
|
|
|
TLSA: ['usage', 'selector', 'mtype', 'cert'],
|
|
|
|
TXT: ['strings'],
|
2017-01-29 07:28:53 +00:00
|
|
|
}
|
2015-08-21 04:22:01 +00:00
|
|
|
|
|
|
|
rd = {}
|
|
|
|
|
|
|
|
if rdata.rdtype in supported_types:
|
|
|
|
fields = supported_types[rdata.rdtype]
|
|
|
|
for f in fields:
|
2017-06-02 11:14:11 +00:00
|
|
|
val = rdata.__getattribute__(f)
|
2015-08-21 04:22:01 +00:00
|
|
|
|
2017-03-26 16:24:30 +00:00
|
|
|
if isinstance(val, dns.name.Name):
|
2015-08-21 04:22:01 +00:00
|
|
|
val = dns.name.Name.to_text(val)
|
|
|
|
|
|
|
|
if rdata.rdtype == DLV and f == 'digest':
|
|
|
|
val = dns.rdata._hexify(rdata.digest).replace(' ', '')
|
|
|
|
if rdata.rdtype == DS and f == 'digest':
|
|
|
|
val = dns.rdata._hexify(rdata.digest).replace(' ', '')
|
|
|
|
if rdata.rdtype == DNSKEY and f == 'key':
|
|
|
|
val = dns.rdata._base64ify(rdata.key).replace(' ', '')
|
|
|
|
if rdata.rdtype == NSEC3PARAM and f == 'salt':
|
|
|
|
val = dns.rdata._hexify(rdata.salt).replace(' ', '')
|
|
|
|
if rdata.rdtype == SSHFP and f == 'fingerprint':
|
|
|
|
val = dns.rdata._hexify(rdata.fingerprint).replace(' ', '')
|
|
|
|
if rdata.rdtype == TLSA and f == 'cert':
|
|
|
|
val = dns.rdata._hexify(rdata.cert).replace(' ', '')
|
|
|
|
|
2017-06-02 11:14:11 +00:00
|
|
|
rd[f] = val
|
2015-08-21 04:22:01 +00:00
|
|
|
|
|
|
|
return rd
|
|
|
|
|
2017-06-02 11:14:11 +00:00
|
|
|
|
2015-08-21 04:22:01 +00:00
|
|
|
# ==============================================================
|
|
|
|
# dig: Lookup DNS records
|
|
|
|
#
|
|
|
|
# --------------------------------------------------------------
|
|
|
|
|
|
|
|
class LookupModule(LookupBase):
|
|
|
|
|
|
|
|
def run(self, terms, variables=None, **kwargs):
|
|
|
|
|
|
|
|
'''
|
|
|
|
terms contains a string with things to `dig' for. We support the
|
|
|
|
following formats:
|
|
|
|
example.com # A record
|
|
|
|
example.com qtype=A # same
|
|
|
|
example.com/TXT # specific qtype
|
|
|
|
example.com qtype=txt # same
|
2016-09-11 09:38:36 +00:00
|
|
|
192.0.2.23/PTR # reverse PTR
|
|
|
|
^^ shortcut for 23.2.0.192.in-addr.arpa/PTR
|
2015-08-21 04:22:01 +00:00
|
|
|
example.net/AAAA @nameserver # query specified server
|
|
|
|
^^^ can be comma-sep list of names/addresses
|
|
|
|
|
|
|
|
... flat=0 # returns a dict; default is 1 == string
|
|
|
|
'''
|
|
|
|
|
2017-02-24 22:49:43 +00:00
|
|
|
if HAVE_DNS is False:
|
2015-08-21 04:22:01 +00:00
|
|
|
raise AnsibleError("Can't LOOKUP(dig): module dns.resolver is not installed")
|
|
|
|
|
|
|
|
# Create Resolver object so that we can set NS if necessary
|
2017-04-04 19:00:56 +00:00
|
|
|
myres = dns.resolver.Resolver(configure=True)
|
2015-08-21 04:22:01 +00:00
|
|
|
edns_size = 4096
|
|
|
|
myres.use_edns(0, ednsflags=dns.flags.DO, payload=edns_size)
|
|
|
|
|
|
|
|
domain = None
|
2017-06-02 11:14:11 +00:00
|
|
|
qtype = 'A'
|
|
|
|
flat = True
|
2017-04-04 19:00:56 +00:00
|
|
|
rdclass = dns.rdataclass.from_text('IN')
|
2015-08-21 04:22:01 +00:00
|
|
|
|
|
|
|
for t in terms:
|
2016-09-11 09:38:36 +00:00
|
|
|
if t.startswith('@'): # e.g. "@10.0.1.2,192.0.2.1" is ok.
|
2015-08-21 04:22:01 +00:00
|
|
|
nsset = t[1:].split(',')
|
|
|
|
for ns in nsset:
|
2017-04-04 19:00:56 +00:00
|
|
|
nameservers = []
|
2015-08-21 04:22:01 +00:00
|
|
|
# Check if we have a valid IP address. If so, use that, otherwise
|
|
|
|
# try to resolve name to address using system's resolver. If that
|
|
|
|
# fails we bail out.
|
|
|
|
try:
|
|
|
|
socket.inet_aton(ns)
|
|
|
|
nameservers.append(ns)
|
|
|
|
except:
|
|
|
|
try:
|
|
|
|
nsaddr = dns.resolver.query(ns)[0].address
|
|
|
|
nameservers.append(nsaddr)
|
2015-08-27 06:16:11 +00:00
|
|
|
except Exception as e:
|
2017-05-19 16:56:55 +00:00
|
|
|
raise AnsibleError("dns lookup NS: %s" % to_native(e))
|
2015-08-21 04:22:01 +00:00
|
|
|
myres.nameservers = nameservers
|
|
|
|
continue
|
|
|
|
if '=' in t:
|
|
|
|
try:
|
|
|
|
opt, arg = t.split('=')
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if opt == 'qtype':
|
|
|
|
qtype = arg.upper()
|
|
|
|
elif opt == 'flat':
|
|
|
|
flat = int(arg)
|
2017-04-04 19:00:56 +00:00
|
|
|
elif opt == 'class':
|
|
|
|
try:
|
|
|
|
rdclass = dns.rdataclass.from_text(arg)
|
|
|
|
except Exception as e:
|
2017-05-19 16:56:55 +00:00
|
|
|
raise AnsibleError("dns lookup illegal CLASS: %s" % to_native(e))
|
2015-08-21 04:22:01 +00:00
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
if '/' in t:
|
|
|
|
try:
|
|
|
|
domain, qtype = t.split('/')
|
|
|
|
except:
|
|
|
|
domain = t
|
|
|
|
else:
|
|
|
|
domain = t
|
|
|
|
|
2017-04-04 19:00:56 +00:00
|
|
|
# print "--- domain = {0} qtype={1} rdclass={2}".format(domain, qtype, rdclass)
|
2015-08-21 04:22:01 +00:00
|
|
|
|
|
|
|
ret = []
|
|
|
|
|
|
|
|
if qtype.upper() == 'PTR':
|
|
|
|
try:
|
|
|
|
n = dns.reversename.from_address(domain)
|
|
|
|
domain = n.to_text()
|
|
|
|
except dns.exception.SyntaxError:
|
|
|
|
pass
|
2015-08-27 06:16:11 +00:00
|
|
|
except Exception as e:
|
2017-05-19 16:56:55 +00:00
|
|
|
raise AnsibleError("dns.reversename unhandled exception %s" % to_native(e))
|
2015-08-21 04:22:01 +00:00
|
|
|
|
|
|
|
try:
|
2017-04-04 19:00:56 +00:00
|
|
|
answers = myres.query(domain, qtype, rdclass=rdclass)
|
2015-08-21 04:22:01 +00:00
|
|
|
for rdata in answers:
|
|
|
|
s = rdata.to_text()
|
|
|
|
if qtype.upper() == 'TXT':
|
|
|
|
s = s[1:-1] # Strip outside quotes on TXT rdata
|
|
|
|
|
|
|
|
if flat:
|
|
|
|
ret.append(s)
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
rd = make_rdata_dict(rdata)
|
2017-06-02 11:14:11 +00:00
|
|
|
rd['owner'] = answers.canonical_name.to_text()
|
|
|
|
rd['type'] = dns.rdatatype.to_text(rdata.rdtype)
|
|
|
|
rd['ttl'] = answers.rrset.ttl
|
|
|
|
rd['class'] = dns.rdataclass.to_text(rdata.rdclass)
|
2015-08-21 04:22:01 +00:00
|
|
|
|
|
|
|
ret.append(rd)
|
2015-08-27 06:16:11 +00:00
|
|
|
except Exception as e:
|
2015-08-21 04:22:01 +00:00
|
|
|
ret.append(str(e))
|
|
|
|
|
|
|
|
except dns.resolver.NXDOMAIN:
|
|
|
|
ret.append('NXDOMAIN')
|
|
|
|
except dns.resolver.NoAnswer:
|
|
|
|
ret.append("")
|
|
|
|
except dns.resolver.Timeout:
|
|
|
|
ret.append('')
|
2015-08-27 06:16:11 +00:00
|
|
|
except dns.exception.DNSException as e:
|
2017-05-19 16:56:55 +00:00
|
|
|
raise AnsibleError("dns.resolver unhandled exception %s" % to_native(e))
|
2015-08-21 04:22:01 +00:00
|
|
|
|
|
|
|
return ret
|