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