Repository: ansible-collections/community.general

Issue:
lookup('dig', arg1) should be able to process lists
##### SUMMARY
lookup('dig', arg) should be able to process arguments that are lists to names to lookup.

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
lookup dig

##### ADDITIONAL INFORMATION
See also https://github.com/ansible/ansible/issues/20470

There was a suggestion there (https://github.com/ansible/ansible/issues/20470#issuecomment-443220060) that you could do:
```
lookup('dig', *list)
```
but that does not work for me - it only returns the IP of the last item in the list.



Discussion:
Files identified in the description:
* [`plugins/lookup/dig.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/lookup/dig.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @jpmens
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@opoplawski Following works for me - 

```yaml
---
- hosts: localhost
  vars:
    servers:
    - google.com
    - yahoo.com
  tasks:
    - debug:
        msg: "{{ lookup('dig', *servers) }}"
```

```
ok: [localhost] => {
    "msg": "74.6.231.21,74.6.143.26,98.137.11.163,74.6.143.25,98.137.11.164,74.6.231.20"
}
```

needs_info
The code only resolves one DNS name, so the last one wins: https://github.com/ansible-collections/community.general/blob/main/plugins/lookup/dig.py#L311 This is consistent with the result @Akasurde shows, all six IP addresses resolve to Yahoo servers (`host -t NS yahoo.com` only gives me five nameservers though), and the four nameservers `google.com` is using have different IPs.
Correct.
@opoplawski This issue is waiting for your response. Please respond or the issue will be closed.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: needs_info_base --->
I think this is still a valid request.
why not `loop: "{{ servers }}"`?
Just lost an hour wondering why I was not getting the correct results. The documentation explicitly states that this module will resolve the domain_(s)_ provided:
https://github.com/ansible-collections/community.general/blob/5470ea30dce806dc6c9d766baa57a9ea57aabcd2/plugins/lookup/dig.py#L29-L31


https://github.com/ansible/ansible/issues/20470#issuecomment-443220060 suggests that this is a regression and that it worked before.
It was never intended by me that dig be able to query multiple domains, and the description clearly states

> The dig lookup runs queries against DNS servers to retrieve DNS records for a specific name 

I think the `(s`) in domains for the `_terms` parameter is a typo.
I'd be happy to take a stab at this. I'm guessing we want to keep the pre-processing phase so that all options (`qtype=`, etc) apply to previous terms as well as following ones (as is currently the case). Special care has to be taken if multiple qtype are specified (i.e. `'google.com', 'yahoo.com/TXT'`).

Maybe it should be an error to specify the same option twice to avoid the surprise where `'qtype=TXT', 'google.com', 'qtype=A', 'yahoo.com'` doesn't error, but doesn't do what one would expect just reading it.

Here is a basic (absolutely untested) patch:

```diff
diff --git a/plugins/lookup/dig.py b/plugins/lookup/dig.py
--- plugins/lookup/dig.py
+++ plugins/lookup/dig.py
@@ -280,9 +280,9 @@
         myres = dns.resolver.Resolver(configure=True)
         edns_size = 4096
         myres.use_edns(0, ednsflags=dns.flags.DO, payload=edns_size)
 
-        domain = None
+        domains = []
         qtype = 'A'
         flat = True
         fail_on_error = False
         rdclass = dns.rdataclass.from_text('IN')
@@ -330,61 +330,67 @@
 
             if '/' in t:
                 try:
                     domain, qtype = t.split('/')
+                    domains.append((domain, qtype))
                 except Exception:
-                    domain = t
+                    domains.append(t)
             else:
-                domain = t
+                domains.append(t)
 
         # print "--- domain = {0} qtype={1} rdclass={2}".format(domain, qtype, rdclass)
 
         ret = []
 
-        if qtype.upper() == 'PTR':
+        for domain in domains:
+            current_qtype = qtype
+            if isinstance(domain, tuple):
+                domain, current_qtype = domain
+
+            if current_qtype.upper() == 'PTR':
+                try:
+                    n = dns.reversename.from_address(domain)
+                    domain = n.to_text()
+                except dns.exception.SyntaxError:
+                    pass
+                except Exception as e:
+                    raise AnsibleError("dns.reversename unhandled exception %s" % to_native(e))
+
             try:
-                n = dns.reversename.from_address(domain)
-                domain = n.to_text()
-            except dns.exception.SyntaxError:
-                pass
-            except Exception as e:
-                raise AnsibleError("dns.reversename unhandled exception %s" % to_native(e))
+                answers = myres.query(domain, current_qtype, rdclass=rdclass)
+                for rdata in answers:
+                    s = rdata.to_text()
+                    if current_qtype.upper() == 'TXT':
+                        s = s[1:-1]  # Strip outside quotes on TXT rdata
 
-        try:
-            answers = myres.query(domain, qtype, rdclass=rdclass)
-            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)
+                            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)
 
-                if flat:
-                    ret.append(s)
-                else:
-                    try:
-                        rd = make_rdata_dict(rdata)
-                        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)
+                            ret.append(rd)
+                        except Exception as err:
+                            if fail_on_error:
+                                raise AnsibleError("Lookup failed: %s" % str(err))
+                            ret.append(str(err))
 
-                        ret.append(rd)
-                    except Exception as err:
-                        if fail_on_error:
-                            raise AnsibleError("Lookup failed: %s" % str(err))
-                        ret.append(str(err))
+            except dns.resolver.NXDOMAIN as err:
+                if fail_on_error:
+                    raise AnsibleError("Lookup failed: %s" % str(err))
+                ret.append('NXDOMAIN')
+            except dns.resolver.NoAnswer as err:
+                if fail_on_error:
+                    raise AnsibleError("Lookup failed: %s" % str(err))
+                ret.append("")
+            except dns.resolver.Timeout as err:
+                if fail_on_error:
+                    raise AnsibleError("Lookup failed: %s" % str(err))
+                ret.append('')
+            except dns.exception.DNSException as err:
+                raise AnsibleError("dns.resolver unhandled exception %s" % to_native(err))
 
-        except dns.resolver.NXDOMAIN as err:
-            if fail_on_error:
-                raise AnsibleError("Lookup failed: %s" % str(err))
-            ret.append('NXDOMAIN')
-        except dns.resolver.NoAnswer as err:
-            if fail_on_error:
-                raise AnsibleError("Lookup failed: %s" % str(err))
-            ret.append("")
-        except dns.resolver.Timeout as err:
-            if fail_on_error:
-                raise AnsibleError("Lookup failed: %s" % str(err))
-            ret.append('')
-        except dns.exception.DNSException as err:
-            raise AnsibleError("dns.resolver unhandled exception %s" % to_native(err))
-
         return ret
```
I'd also like to re-iterate that this wouldn't be required if there was another way to apply a lookup to a list (being able to write `somelist | map('lookup', 'dig')` would avoid having to solve this over and over again).
I just found this issue after preparing a PR to add support for multiple domains in the query... I should have looked here first :-) I agree with @bendem that `map('lookup', 'dig', ...)` would be incredibly useful but until the Ansible core team decides to implement lookup support in `map`, we're stuck with doing it in the plugins themselves.

PR will be opened in a moment, stay tuned.

Resolving patch:
diff --git a/plugins/lookup/dig.py b/plugins/lookup/dig.py
--- a/plugins/lookup/dig.py
+++ b/plugins/lookup/dig.py
@@ -61,6 +61,7 @@
         description:
           - Return empty result without empty strings, and return empty list instead of C(NXDOMAIN).
           - The default for this option will likely change to C(true) in the future.
+          - This option will be forced to C(true) if multiple domains to be queried are specified.
         default: false
         type: bool
         version_added: 6.0.0
@@ -95,6 +96,21 @@
     msg: "MX record for gmail.com {{ item }}"
   with_items: "{{ lookup('community.general.dig', 'gmail.com./MX', wantlist=true) }}"
 
+- name: Lookup multiple names at once
+  ansible.builtin.debug:
+    msg: "A record found {{ item }}"
+  loop: "{{ query('community.general.dig', 'example.org.', 'example.com.', 'gmail.com.') }}"
+
+- name: Lookup multiple names at once (from list variable)
+  ansible.builtin.debug:
+    msg: "A record found {{ item }}"
+  loop: "{{ query('community.general.dig', *hosts) }}"
+  vars:
+    hosts:
+      - example.org.
+      - example.com.
+      - gmail.com.
+
 - ansible.builtin.debug:
     msg: "Reverse DNS for 192.0.2.5 is {{ lookup('community.general.dig', '192.0.2.5/PTR') }}"
 - ansible.builtin.debug:
@@ -308,7 +324,7 @@ def run(self, terms, variables=None, **kwargs):
         edns_size = 4096
         myres.use_edns(0, ednsflags=dns.flags.DO, payload=edns_size)
 
-        domain = None
+        domains = []
         qtype = self.get_option('qtype')
         flat = self.get_option('flat')
         fail_on_error = self.get_option('fail_on_error')
@@ -365,63 +381,71 @@ def run(self, terms, variables=None, **kwargs):
             if '/' in t:
                 try:
                     domain, qtype = t.split('/')
+                    domains.append(domain)
                 except Exception:
-                    domain = t
+                    domains.append(t)
             else:
-                domain = t
+                domains.append(t)
 
         # print "--- domain = {0} qtype={1} rdclass={2}".format(domain, qtype, rdclass)
 
+        if qtype.upper() == 'PTR':
+            reversed_domains = []
+            for domain in domains:
+                try:
+                    n = dns.reversename.from_address(domain)
+                    reversed_domains.append(n.to_text())
+                except dns.exception.SyntaxError:
+                    pass
+                except Exception as e:
+                    raise AnsibleError("dns.reversename unhandled exception %s" % to_native(e))
+            domains = reversed_domains
+
+        if len(domains) > 1:
+            real_empty = True
+
         ret = []
 
-        if qtype.upper() == 'PTR':
+        for domain in domains:
             try:
-                n = dns.reversename.from_address(domain)
-                domain = n.to_text()
-            except dns.exception.SyntaxError:
-                pass
-            except Exception as e:
-                raise AnsibleError("dns.reversename unhandled exception %s" % to_native(e))
-
-        try:
-            answers = myres.query(domain, qtype, rdclass=rdclass)
-            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)
-                        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)
-
-                        ret.append(rd)
-                    except Exception as err:
-                        if fail_on_error:
-                            raise AnsibleError("Lookup failed: %s" % str(err))
-                        ret.append(str(err))
-
-        except dns.resolver.NXDOMAIN as err:
-            if fail_on_error:
-                raise AnsibleError("Lookup failed: %s" % str(err))
-            if not real_empty:
-                ret.append('NXDOMAIN')
-        except dns.resolver.NoAnswer as err:
-            if fail_on_error:
-                raise AnsibleError("Lookup failed: %s" % str(err))
-            if not real_empty:
-                ret.append("")
-        except dns.resolver.Timeout as err:
-            if fail_on_error:
-                raise AnsibleError("Lookup failed: %s" % str(err))
-            if not real_empty:
-                ret.append("")
-        except dns.exception.DNSException as err:
-            raise AnsibleError("dns.resolver unhandled exception %s" % to_native(err))
+                answers = myres.query(domain, qtype, rdclass=rdclass)
+                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)
+                            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)
+
+                            ret.append(rd)
+                        except Exception as err:
+                            if fail_on_error:
+                                raise AnsibleError("Lookup failed: %s" % str(err))
+                            ret.append(str(err))
+
+            except dns.resolver.NXDOMAIN as err:
+                if fail_on_error:
+                    raise AnsibleError("Lookup failed: %s" % str(err))
+                if not real_empty:
+                    ret.append('NXDOMAIN')
+            except dns.resolver.NoAnswer as err:
+                if fail_on_error:
+                    raise AnsibleError("Lookup failed: %s" % str(err))
+                if not real_empty:
+                    ret.append("")
+            except dns.resolver.Timeout as err:
+                if fail_on_error:
+                    raise AnsibleError("Lookup failed: %s" % str(err))
+                if not real_empty:
+                    ret.append("")
+            except dns.exception.DNSException as err:
+                raise AnsibleError("dns.resolver unhandled exception %s" % to_native(err))
 
         return ret


Test patch: