1# (c) 2015, Jan-Piet Mens <jpmens(at)gmail.com>
2# (c) 2017 Ansible Project
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4from __future__ import (absolute_import, division, print_function)
5__metaclass__ = type
6
7DOCUMENTATION = """
8    lookup: dig
9    author: Jan-Piet Mens (@jpmens) <jpmens(at)gmail.com>
10    version_added: "1.9"
11    short_description: query DNS using the dnspython library
12    requirements:
13      - dnspython (python library, http://www.dnspython.org/)
14    description:
15      - The dig lookup runs queries against DNS servers to retrieve DNS records for a specific name (FQDN - fully qualified domain name).
16        It is possible to lookup any DNS record in this manner.
17      - There is a couple of different syntaxes that can be used to specify what record should be retrieved, and for which name.
18         It is also possible to explicitly specify the DNS server(s) to use for lookups.
19      - In its simplest form, the dig lookup plugin can be used to retrieve an IPv4 address (DNS A record) associated with FQDN
20      - In addition to (default) A record, it is also possible to specify a different record type that should be queried.
21        This can be done by either passing-in additional parameter of format qtype=TYPE to the dig lookup, or by appending /TYPE to the FQDN being queried.
22      - If multiple values are associated with the requested record, the results will be returned as a comma-separated list.
23        In such cases you may want to pass option wantlist=True to the plugin, which will result in the record values being returned as a list
24        over which you can iterate later on.
25      - By default, the lookup will rely on system-wide configured DNS servers for performing the query.
26        It is also possible to explicitly specify DNS servers to query using the @DNS_SERVER_1,DNS_SERVER_2,...,DNS_SERVER_N notation.
27        This needs to be passed-in as an additional parameter to the lookup
28    options:
29      _terms:
30        description: domain(s) to query
31      qtype:
32        description: record type to query
33        default: 'A'
34        choices: [A, ALL, AAAA, CNAME, DNAME, DLV, DNSKEY, DS, HINFO, LOC, MX, NAPTR, NS, NSEC3PARAM, PTR, RP, RRSIG, SOA, SPF, SRV, SSHFP, TLSA, TXT]
35      flat:
36        description: If 0 each record is returned as a dictionary, otherwise a string
37        default: 1
38    notes:
39      - ALL is not a record per-se, merely the listed fields are available for any record results you retrieve in the form of a dictionary.
40      - While the 'dig' lookup plugin supports anything which dnspython supports out of the box, only a subset can be converted into a dictionary.
41      - If you need to obtain the AAAA record (IPv6 address), you must specify the record type explicitly.
42        Syntax for specifying the record type is shown in the examples below.
43      - The trailing dot in most of the examples listed is purely optional, but is specified for completeness/correctness sake.
44"""
45
46EXAMPLES = """
47- name: Simple A record (IPV4 address) lookup for example.com
48  debug: msg="{{ lookup('dig', 'example.com.')}}"
49
50- name: "The TXT record for example.org."
51  debug: msg="{{ lookup('dig', 'example.org.', 'qtype=TXT') }}"
52
53- name: "The TXT record for example.org, alternative syntax."
54  debug: msg="{{ lookup('dig', 'example.org./TXT') }}"
55
56- name: use in a loop
57  debug: msg="MX record for gmail.com {{ item }}"
58  with_items: "{{ lookup('dig', 'gmail.com./MX', wantlist=True) }}"
59
60- debug: msg="Reverse DNS for 192.0.2.5 is {{ lookup('dig', '192.0.2.5/PTR') }}"
61- debug: msg="Reverse DNS for 192.0.2.5 is {{ lookup('dig', '5.2.0.192.in-addr.arpa./PTR') }}"
62- debug: msg="Reverse DNS for 192.0.2.5 is {{ lookup('dig', '5.2.0.192.in-addr.arpa.', 'qtype=PTR') }}"
63- debug: msg="Querying 198.51.100.23 for IPv4 address for example.com. produces {{ lookup('dig', 'example.com', '@198.51.100.23') }}"
64
65- debug: msg="XMPP service for gmail.com. is available at {{ item.target }} on port {{ item.port }}"
66  with_items: "{{ lookup('dig', '_xmpp-server._tcp.gmail.com./SRV', 'flat=0', wantlist=True) }}"
67"""
68
69RETURN = """
70  _list:
71    description:
72      - list of composed strings or dictonaries with key and value
73        If a dictionary, fields shows the keys returned depending on query type
74    fields:
75       ALL: owner, ttl, type
76       A: address
77       AAAA: address
78       CNAME: target
79       DNAME: target
80       DLV: algorithm, digest_type, key_tag, digest
81       DNSKEY: flags, algorithm, protocol, key
82       DS: algorithm, digest_type, key_tag, digest
83       HINFO:  cpu, os
84       LOC: latitude, longitude, altitude, size, horizontal_precision, vertical_precision
85       MX: preference, exchange
86       NAPTR: order, preference, flags, service, regexp, replacement
87       NS: target
88       NSEC3PARAM: algorithm, flags, iterations, salt
89       PTR: target
90       RP: mbox, txt
91       SOA: mname, rname, serial, refresh, retry, expire, minimum
92       SPF: strings
93       SRV: priority, weight, port, target
94       SSHFP: algorithm, fp_type, fingerprint
95       TLSA: usage, selector, mtype, cert
96       TXT: strings
97"""
98
99from ansible.errors import AnsibleError
100from ansible.plugins.lookup import LookupBase
101from ansible.module_utils._text import to_native
102import socket
103
104try:
105    import dns.exception
106    import dns.name
107    import dns.resolver
108    import dns.reversename
109    import dns.rdataclass
110    from dns.rdatatype import (A, AAAA, CNAME, DLV, DNAME, DNSKEY, DS, HINFO, LOC,
111                               MX, NAPTR, NS, NSEC3PARAM, PTR, RP, SOA, SPF, SRV, SSHFP, TLSA, TXT)
112    HAVE_DNS = True
113except ImportError:
114    HAVE_DNS = False
115
116
117def make_rdata_dict(rdata):
118    ''' While the 'dig' lookup plugin supports anything which dnspython supports
119        out of the box, the following supported_types list describes which
120        DNS query types we can convert to a dict.
121
122        Note: adding support for RRSIG is hard work. :)
123    '''
124    supported_types = {
125        A: ['address'],
126        AAAA: ['address'],
127        CNAME: ['target'],
128        DNAME: ['target'],
129        DLV: ['algorithm', 'digest_type', 'key_tag', 'digest'],
130        DNSKEY: ['flags', 'algorithm', 'protocol', 'key'],
131        DS: ['algorithm', 'digest_type', 'key_tag', 'digest'],
132        HINFO: ['cpu', 'os'],
133        LOC: ['latitude', 'longitude', 'altitude', 'size', 'horizontal_precision', 'vertical_precision'],
134        MX: ['preference', 'exchange'],
135        NAPTR: ['order', 'preference', 'flags', 'service', 'regexp', 'replacement'],
136        NS: ['target'],
137        NSEC3PARAM: ['algorithm', 'flags', 'iterations', 'salt'],
138        PTR: ['target'],
139        RP: ['mbox', 'txt'],
140        # RRSIG: ['algorithm', 'labels', 'original_ttl', 'expiration', 'inception', 'signature'],
141        SOA: ['mname', 'rname', 'serial', 'refresh', 'retry', 'expire', 'minimum'],
142        SPF: ['strings'],
143        SRV: ['priority', 'weight', 'port', 'target'],
144        SSHFP: ['algorithm', 'fp_type', 'fingerprint'],
145        TLSA: ['usage', 'selector', 'mtype', 'cert'],
146        TXT: ['strings'],
147    }
148
149    rd = {}
150
151    if rdata.rdtype in supported_types:
152        fields = supported_types[rdata.rdtype]
153        for f in fields:
154            val = rdata.__getattribute__(f)
155
156            if isinstance(val, dns.name.Name):
157                val = dns.name.Name.to_text(val)
158
159            if rdata.rdtype == DLV and f == 'digest':
160                val = dns.rdata._hexify(rdata.digest).replace(' ', '')
161            if rdata.rdtype == DS and f == 'digest':
162                val = dns.rdata._hexify(rdata.digest).replace(' ', '')
163            if rdata.rdtype == DNSKEY and f == 'key':
164                val = dns.rdata._base64ify(rdata.key).replace(' ', '')
165            if rdata.rdtype == NSEC3PARAM and f == 'salt':
166                val = dns.rdata._hexify(rdata.salt).replace(' ', '')
167            if rdata.rdtype == SSHFP and f == 'fingerprint':
168                val = dns.rdata._hexify(rdata.fingerprint).replace(' ', '')
169            if rdata.rdtype == TLSA and f == 'cert':
170                val = dns.rdata._hexify(rdata.cert).replace(' ', '')
171
172            rd[f] = val
173
174    return rd
175
176
177# ==============================================================
178# dig: Lookup DNS records
179#
180# --------------------------------------------------------------
181
182class LookupModule(LookupBase):
183
184    def run(self, terms, variables=None, **kwargs):
185
186        '''
187        terms contains a string with things to `dig' for. We support the
188        following formats:
189            example.com                                     # A record
190            example.com  qtype=A                            # same
191            example.com/TXT                                 # specific qtype
192            example.com  qtype=txt                          # same
193            192.0.2.23/PTR                                 # reverse PTR
194              ^^ shortcut for 23.2.0.192.in-addr.arpa/PTR
195            example.net/AAAA  @nameserver                   # query specified server
196                               ^^^ can be comma-sep list of names/addresses
197
198            ... flat=0                                      # returns a dict; default is 1 == string
199        '''
200
201        if HAVE_DNS is False:
202            raise AnsibleError("The dig lookup requires the python 'dnspython' library and it is not installed")
203
204        # Create Resolver object so that we can set NS if necessary
205        myres = dns.resolver.Resolver(configure=True)
206        edns_size = 4096
207        myres.use_edns(0, ednsflags=dns.flags.DO, payload=edns_size)
208
209        domain = None
210        qtype = 'A'
211        flat = True
212        rdclass = dns.rdataclass.from_text('IN')
213
214        for t in terms:
215            if t.startswith('@'):       # e.g. "@10.0.1.2,192.0.2.1" is ok.
216                nsset = t[1:].split(',')
217                for ns in nsset:
218                    nameservers = []
219                    # Check if we have a valid IP address. If so, use that, otherwise
220                    # try to resolve name to address using system's resolver. If that
221                    # fails we bail out.
222                    try:
223                        socket.inet_aton(ns)
224                        nameservers.append(ns)
225                    except Exception:
226                        try:
227                            nsaddr = dns.resolver.query(ns)[0].address
228                            nameservers.append(nsaddr)
229                        except Exception as e:
230                            raise AnsibleError("dns lookup NS: %s" % to_native(e))
231                    myres.nameservers = nameservers
232                continue
233            if '=' in t:
234                try:
235                    opt, arg = t.split('=')
236                except Exception:
237                    pass
238
239                if opt == 'qtype':
240                    qtype = arg.upper()
241                elif opt == 'flat':
242                    flat = int(arg)
243                elif opt == 'class':
244                    try:
245                        rdclass = dns.rdataclass.from_text(arg)
246                    except Exception as e:
247                        raise AnsibleError("dns lookup illegal CLASS: %s" % to_native(e))
248
249                continue
250
251            if '/' in t:
252                try:
253                    domain, qtype = t.split('/')
254                except Exception:
255                    domain = t
256            else:
257                domain = t
258
259        # print "--- domain = {0} qtype={1} rdclass={2}".format(domain, qtype, rdclass)
260
261        ret = []
262
263        if qtype.upper() == 'PTR':
264            try:
265                n = dns.reversename.from_address(domain)
266                domain = n.to_text()
267            except dns.exception.SyntaxError:
268                pass
269            except Exception as e:
270                raise AnsibleError("dns.reversename unhandled exception %s" % to_native(e))
271
272        try:
273            answers = myres.query(domain, qtype, rdclass=rdclass)
274            for rdata in answers:
275                s = rdata.to_text()
276                if qtype.upper() == 'TXT':
277                    s = s[1:-1]  # Strip outside quotes on TXT rdata
278
279                if flat:
280                    ret.append(s)
281                else:
282                    try:
283                        rd = make_rdata_dict(rdata)
284                        rd['owner'] = answers.canonical_name.to_text()
285                        rd['type'] = dns.rdatatype.to_text(rdata.rdtype)
286                        rd['ttl'] = answers.rrset.ttl
287                        rd['class'] = dns.rdataclass.to_text(rdata.rdclass)
288
289                        ret.append(rd)
290                    except Exception as e:
291                        ret.append(str(e))
292
293        except dns.resolver.NXDOMAIN:
294            ret.append('NXDOMAIN')
295        except dns.resolver.NoAnswer:
296            ret.append("")
297        except dns.resolver.Timeout:
298            ret.append('')
299        except dns.exception.DNSException as e:
300            raise AnsibleError("dns.resolver unhandled exception %s" % to_native(e))
301
302        return ret
303