1# -*- coding: utf-8 -*- 2# (c) 2015, Jan-Piet Mens <jpmens(at)gmail.com> 3# (c) 2017 Ansible Project 4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5from __future__ import (absolute_import, division, print_function) 6__metaclass__ = type 7 8DOCUMENTATION = ''' 9 name: dig 10 author: Jan-Piet Mens (@jpmens) <jpmens(at)gmail.com> 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 retry_servfail: 39 description: Retry a nameserver if it returns SERVFAIL. 40 default: false 41 type: bool 42 version_added: 3.6.0 43 notes: 44 - 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. 45 - While the 'dig' lookup plugin supports anything which dnspython supports out of the box, only a subset can be converted into a dictionary. 46 - If you need to obtain the AAAA record (IPv6 address), you must specify the record type explicitly. 47 Syntax for specifying the record type is shown in the examples below. 48 - The trailing dot in most of the examples listed is purely optional, but is specified for completeness/correctness sake. 49''' 50 51EXAMPLES = """ 52- name: Simple A record (IPV4 address) lookup for example.com 53 ansible.builtin.debug: 54 msg: "{{ lookup('community.general.dig', 'example.com.')}}" 55 56- name: "The TXT record for example.org." 57 ansible.builtin.debug: 58 msg: "{{ lookup('community.general.dig', 'example.org.', 'qtype=TXT') }}" 59 60- name: "The TXT record for example.org, alternative syntax." 61 ansible.builtin.debug: 62 msg: "{{ lookup('community.general.dig', 'example.org./TXT') }}" 63 64- name: use in a loop 65 ansible.builtin.debug: 66 msg: "MX record for gmail.com {{ item }}" 67 with_items: "{{ lookup('community.general.dig', 'gmail.com./MX', wantlist=True) }}" 68 69- ansible.builtin.debug: 70 msg: "Reverse DNS for 192.0.2.5 is {{ lookup('community.general.dig', '192.0.2.5/PTR') }}" 71- ansible.builtin.debug: 72 msg: "Reverse DNS for 192.0.2.5 is {{ lookup('community.general.dig', '5.2.0.192.in-addr.arpa./PTR') }}" 73- ansible.builtin.debug: 74 msg: "Reverse DNS for 192.0.2.5 is {{ lookup('community.general.dig', '5.2.0.192.in-addr.arpa.', 'qtype=PTR') }}" 75- ansible.builtin.debug: 76 msg: "Querying 198.51.100.23 for IPv4 address for example.com. produces {{ lookup('dig', 'example.com', '@198.51.100.23') }}" 77 78- ansible.builtin.debug: 79 msg: "XMPP service for gmail.com. is available at {{ item.target }} on port {{ item.port }}" 80 with_items: "{{ lookup('community.general.dig', '_xmpp-server._tcp.gmail.com./SRV', 'flat=0', wantlist=True) }}" 81 82- name: Retry nameservers that return SERVFAIL 83 ansible.builtin.debug: 84 msg: "{{ lookup('community.general.dig', 'example.org./A', 'retry_servfail=True') }}" 85""" 86 87RETURN = """ 88 _list: 89 description: 90 - List of composed strings or dictionaries with key and value 91 If a dictionary, fields shows the keys returned depending on query type 92 type: list 93 elements: raw 94 contains: 95 ALL: 96 description: 97 - owner, ttl, type 98 A: 99 description: 100 - address 101 AAAA: 102 description: 103 - address 104 CNAME: 105 description: 106 - target 107 DNAME: 108 description: 109 - target 110 DLV: 111 description: 112 - algorithm, digest_type, key_tag, digest 113 DNSKEY: 114 description: 115 - flags, algorithm, protocol, key 116 DS: 117 description: 118 - algorithm, digest_type, key_tag, digest 119 HINFO: 120 description: 121 - cpu, os 122 LOC: 123 description: 124 - latitude, longitude, altitude, size, horizontal_precision, vertical_precision 125 MX: 126 description: 127 - preference, exchange 128 NAPTR: 129 description: 130 - order, preference, flags, service, regexp, replacement 131 NS: 132 description: 133 - target 134 NSEC3PARAM: 135 description: 136 - algorithm, flags, iterations, salt 137 PTR: 138 description: 139 - target 140 RP: 141 description: 142 - mbox, txt 143 SOA: 144 description: 145 - mname, rname, serial, refresh, retry, expire, minimum 146 SPF: 147 description: 148 - strings 149 SRV: 150 description: 151 - priority, weight, port, target 152 SSHFP: 153 description: 154 - algorithm, fp_type, fingerprint 155 TLSA: 156 description: 157 - usage, selector, mtype, cert 158 TXT: 159 description: 160 - strings 161""" 162 163from ansible.errors import AnsibleError 164from ansible.plugins.lookup import LookupBase 165from ansible.module_utils.common.text.converters import to_native 166import socket 167 168try: 169 import dns.exception 170 import dns.name 171 import dns.resolver 172 import dns.reversename 173 import dns.rdataclass 174 from dns.rdatatype import (A, AAAA, CNAME, DLV, DNAME, DNSKEY, DS, HINFO, LOC, 175 MX, NAPTR, NS, NSEC3PARAM, PTR, RP, SOA, SPF, SRV, SSHFP, TLSA, TXT) 176 HAVE_DNS = True 177except ImportError: 178 HAVE_DNS = False 179 180 181def make_rdata_dict(rdata): 182 ''' While the 'dig' lookup plugin supports anything which dnspython supports 183 out of the box, the following supported_types list describes which 184 DNS query types we can convert to a dict. 185 186 Note: adding support for RRSIG is hard work. :) 187 ''' 188 supported_types = { 189 A: ['address'], 190 AAAA: ['address'], 191 CNAME: ['target'], 192 DNAME: ['target'], 193 DLV: ['algorithm', 'digest_type', 'key_tag', 'digest'], 194 DNSKEY: ['flags', 'algorithm', 'protocol', 'key'], 195 DS: ['algorithm', 'digest_type', 'key_tag', 'digest'], 196 HINFO: ['cpu', 'os'], 197 LOC: ['latitude', 'longitude', 'altitude', 'size', 'horizontal_precision', 'vertical_precision'], 198 MX: ['preference', 'exchange'], 199 NAPTR: ['order', 'preference', 'flags', 'service', 'regexp', 'replacement'], 200 NS: ['target'], 201 NSEC3PARAM: ['algorithm', 'flags', 'iterations', 'salt'], 202 PTR: ['target'], 203 RP: ['mbox', 'txt'], 204 # RRSIG: ['algorithm', 'labels', 'original_ttl', 'expiration', 'inception', 'signature'], 205 SOA: ['mname', 'rname', 'serial', 'refresh', 'retry', 'expire', 'minimum'], 206 SPF: ['strings'], 207 SRV: ['priority', 'weight', 'port', 'target'], 208 SSHFP: ['algorithm', 'fp_type', 'fingerprint'], 209 TLSA: ['usage', 'selector', 'mtype', 'cert'], 210 TXT: ['strings'], 211 } 212 213 rd = {} 214 215 if rdata.rdtype in supported_types: 216 fields = supported_types[rdata.rdtype] 217 for f in fields: 218 val = rdata.__getattribute__(f) 219 220 if isinstance(val, dns.name.Name): 221 val = dns.name.Name.to_text(val) 222 223 if rdata.rdtype == DLV and f == 'digest': 224 val = dns.rdata._hexify(rdata.digest).replace(' ', '') 225 if rdata.rdtype == DS and f == 'digest': 226 val = dns.rdata._hexify(rdata.digest).replace(' ', '') 227 if rdata.rdtype == DNSKEY and f == 'key': 228 val = dns.rdata._base64ify(rdata.key).replace(' ', '') 229 if rdata.rdtype == NSEC3PARAM and f == 'salt': 230 val = dns.rdata._hexify(rdata.salt).replace(' ', '') 231 if rdata.rdtype == SSHFP and f == 'fingerprint': 232 val = dns.rdata._hexify(rdata.fingerprint).replace(' ', '') 233 if rdata.rdtype == TLSA and f == 'cert': 234 val = dns.rdata._hexify(rdata.cert).replace(' ', '') 235 236 rd[f] = val 237 238 return rd 239 240 241# ============================================================== 242# dig: Lookup DNS records 243# 244# -------------------------------------------------------------- 245 246class LookupModule(LookupBase): 247 248 def run(self, terms, variables=None, **kwargs): 249 250 ''' 251 terms contains a string with things to `dig' for. We support the 252 following formats: 253 example.com # A record 254 example.com qtype=A # same 255 example.com/TXT # specific qtype 256 example.com qtype=txt # same 257 192.0.2.23/PTR # reverse PTR 258 ^^ shortcut for 23.2.0.192.in-addr.arpa/PTR 259 example.net/AAAA @nameserver # query specified server 260 ^^^ can be comma-sep list of names/addresses 261 262 ... flat=0 # returns a dict; default is 1 == string 263 ''' 264 265 if HAVE_DNS is False: 266 raise AnsibleError("The dig lookup requires the python 'dnspython' library and it is not installed") 267 268 # Create Resolver object so that we can set NS if necessary 269 myres = dns.resolver.Resolver(configure=True) 270 edns_size = 4096 271 myres.use_edns(0, ednsflags=dns.flags.DO, payload=edns_size) 272 273 domain = None 274 qtype = 'A' 275 flat = True 276 rdclass = dns.rdataclass.from_text('IN') 277 278 for t in terms: 279 if t.startswith('@'): # e.g. "@10.0.1.2,192.0.2.1" is ok. 280 nsset = t[1:].split(',') 281 for ns in nsset: 282 nameservers = [] 283 # Check if we have a valid IP address. If so, use that, otherwise 284 # try to resolve name to address using system's resolver. If that 285 # fails we bail out. 286 try: 287 socket.inet_aton(ns) 288 nameservers.append(ns) 289 except Exception: 290 try: 291 nsaddr = dns.resolver.query(ns)[0].address 292 nameservers.append(nsaddr) 293 except Exception as e: 294 raise AnsibleError("dns lookup NS: %s" % to_native(e)) 295 myres.nameservers = nameservers 296 continue 297 if '=' in t: 298 try: 299 opt, arg = t.split('=') 300 except Exception: 301 pass 302 303 if opt == 'qtype': 304 qtype = arg.upper() 305 elif opt == 'flat': 306 flat = int(arg) 307 elif opt == 'class': 308 try: 309 rdclass = dns.rdataclass.from_text(arg) 310 except Exception as e: 311 raise AnsibleError("dns lookup illegal CLASS: %s" % to_native(e)) 312 elif opt == 'retry_servfail': 313 myres.retry_servfail = bool(arg) 314 315 continue 316 317 if '/' in t: 318 try: 319 domain, qtype = t.split('/') 320 except Exception: 321 domain = t 322 else: 323 domain = t 324 325 # print "--- domain = {0} qtype={1} rdclass={2}".format(domain, qtype, rdclass) 326 327 ret = [] 328 329 if qtype.upper() == 'PTR': 330 try: 331 n = dns.reversename.from_address(domain) 332 domain = n.to_text() 333 except dns.exception.SyntaxError: 334 pass 335 except Exception as e: 336 raise AnsibleError("dns.reversename unhandled exception %s" % to_native(e)) 337 338 try: 339 answers = myres.query(domain, qtype, rdclass=rdclass) 340 for rdata in answers: 341 s = rdata.to_text() 342 if qtype.upper() == 'TXT': 343 s = s[1:-1] # Strip outside quotes on TXT rdata 344 345 if flat: 346 ret.append(s) 347 else: 348 try: 349 rd = make_rdata_dict(rdata) 350 rd['owner'] = answers.canonical_name.to_text() 351 rd['type'] = dns.rdatatype.to_text(rdata.rdtype) 352 rd['ttl'] = answers.rrset.ttl 353 rd['class'] = dns.rdataclass.to_text(rdata.rdclass) 354 355 ret.append(rd) 356 except Exception as e: 357 ret.append(str(e)) 358 359 except dns.resolver.NXDOMAIN: 360 ret.append('NXDOMAIN') 361 except dns.resolver.NoAnswer: 362 ret.append("") 363 except dns.resolver.Timeout: 364 ret.append('') 365 except dns.exception.DNSException as e: 366 raise AnsibleError("dns.resolver unhandled exception %s" % to_native(e)) 367 368 return ret 369