From daca3ade99625bd86a120ca548e7698b7dce0345 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Loyet?=
 <822436+fatpat@users.noreply.github.com>
Date: Wed, 9 Sep 2020 20:40:48 +0200
Subject: [PATCH] Allow list of filters for the setup module (#68551)

The setup module can now filter out multiple pattern by providing a list
to the filter parameter instead of just a string. Single string sill
works. Previous behaviour remains.

(cherry picked from commit b5c36dac483fdd74d6c570d77cc8f3e396720366)
---
 ...w_list_of_filters_for_the_setup_module.yml |  2 ++
 .../porting_guide_base_2.11.rst               |  1 +
 .../module_utils/facts/ansible_collector.py   | 10 +++++++---
 lib/ansible/modules/setup.py                  | 20 ++++++++++++++++---
 .../gathering_facts/test_gathering_facts.yml  | 18 +++++++++++++++++
 test/sanity/ignore.txt                        |  1 -
 6 files changed, 45 insertions(+), 7 deletions(-)
 create mode 100644 changelogs/fragments/68551_allow_list_of_filters_for_the_setup_module.yml

diff --git a/changelogs/fragments/68551_allow_list_of_filters_for_the_setup_module.yml b/changelogs/fragments/68551_allow_list_of_filters_for_the_setup_module.yml
new file mode 100644
index 00000000000..115690ef35b
--- /dev/null
+++ b/changelogs/fragments/68551_allow_list_of_filters_for_the_setup_module.yml
@@ -0,0 +1,2 @@
+minor_changes:
+  - setup - allow list of filters (https://github.com/ansible/ansible/pull/68551).
diff --git a/docs/docsite/rst/porting_guides/porting_guide_base_2.11.rst b/docs/docsite/rst/porting_guides/porting_guide_base_2.11.rst
index e554aa2ab5f..bbb97278d49 100644
--- a/docs/docsite/rst/porting_guides/porting_guide_base_2.11.rst
+++ b/docs/docsite/rst/porting_guides/porting_guide_base_2.11.rst
@@ -61,6 +61,7 @@ Noteworthy module changes
 
 * facts - On NetBSD, ``ansible_virtualization_type`` now tries to report a more accurate result than ``xen`` when virtualized and not running on Xen.
 * facts - Virtualization facts now include ``virtualization_tech_guest`` and ``virtualization_tech_host`` keys. These are lists of virtualization technologies that a guest is a part of, or that a host provides, respectively. As an example, a host may be set up to provide both KVM and VirtualBox, and these will be included in ``virtualization_tech_host``, and a podman container running on a VM powered by KVM will have a ``virtualization_tech_guest`` of ``["kvm", "podman", "container"]``.
+* The parameter ``filter`` type is changed from ``string`` to ``list`` in the :ref:`setup <setup_module>` module in order to use more than one filter. Previous behaviour (using a ``string``) still remains and works as a single filter.
 
 
 Plugins
diff --git a/lib/ansible/module_utils/facts/ansible_collector.py b/lib/ansible/module_utils/facts/ansible_collector.py
index 8ca0089efae..7f6f576f9c2 100644
--- a/lib/ansible/module_utils/facts/ansible_collector.py
+++ b/lib/ansible/module_utils/facts/ansible_collector.py
@@ -34,6 +34,7 @@ import sys
 
 from ansible.module_utils.facts import timeout
 from ansible.module_utils.facts import collector
+from ansible.module_utils.common.collections import is_string
 
 
 class AnsibleFactCollector(collector.BaseFactCollector):
@@ -53,11 +54,14 @@ class AnsibleFactCollector(collector.BaseFactCollector):
         self.filter_spec = filter_spec
 
     def _filter(self, facts_dict, filter_spec):
-        # assume a filter_spec='' is equilv to filter_spec='*'
+        # assume filter_spec='' or filter_spec=[] is equivalent to filter_spec='*'
         if not filter_spec or filter_spec == '*':
             return facts_dict
 
-        return [(x, y) for x, y in facts_dict.items() if fnmatch.fnmatch(x, filter_spec)]
+        if is_string(filter_spec):
+            filter_spec = [filter_spec]
+
+        return [(x, y) for x, y in facts_dict.items() for f in filter_spec if not f or fnmatch.fnmatch(x, f)]
 
     def collect(self, module=None, collected_facts=None):
         collected_facts = collected_facts or {}
@@ -111,7 +115,7 @@ def get_ansible_collector(all_collector_classes,
                           gather_timeout=None,
                           minimal_gather_subset=None):
 
-    filter_spec = filter_spec or '*'
+    filter_spec = filter_spec or []
     gather_subset = gather_subset or ['all']
     gather_timeout = gather_timeout or timeout.DEFAULT_GATHER_TIMEOUT
     minimal_gather_subset = minimal_gather_subset or frozenset()
diff --git a/lib/ansible/modules/setup.py b/lib/ansible/modules/setup.py
index c7f16c30fa5..efe6780c286 100644
--- a/lib/ansible/modules/setup.py
+++ b/lib/ansible/modules/setup.py
@@ -39,9 +39,16 @@ options:
     filter:
         version_added: "1.1"
         description:
-            - If supplied, only return facts that match this shell-style (fnmatch) wildcard.
+            - If supplied, only return facts that match one of the shell-style
+              (fnmatch) pattern. An empty list basically means 'no filter'.
+              As of Ansible 2.11, the type has changed from string to list
+              and the default has became an empty list. A simple string is
+              still accepted and works as a single pattern. The behaviour
+              prior to Ansible 2.11 remains.
         required: false
-        default: "*"
+        type: list
+        elements: str
+        default: []
     fact_path:
         version_added: "1.3"
         description:
@@ -105,6 +112,13 @@ EXAMPLES = """
       - '!any'
       - facter
 
+- name: Collect only selected facts
+  setup:
+    filter:
+      - 'ansible_distribution'
+      - 'ansible_machine_id'
+      - 'ansible_*_mb'
+
 # Display only facts about certain interfaces.
 # ansible all -m setup -a 'filter=ansible_eth[0-2]'
 
@@ -141,7 +155,7 @@ def main():
         argument_spec=dict(
             gather_subset=dict(default=["all"], required=False, type='list'),
             gather_timeout=dict(default=10, required=False, type='int'),
-            filter=dict(default="*", required=False),
+            filter=dict(default=[], required=False, type='list', elements='str'),
             fact_path=dict(default='/etc/ansible/facts.d', required=False, type='path'),
         ),
         supports_check_mode=True,
diff --git a/test/integration/targets/gathering_facts/test_gathering_facts.yml b/test/integration/targets/gathering_facts/test_gathering_facts.yml
index d4364d29513..0939cba702a 100644
--- a/test/integration/targets/gathering_facts/test_gathering_facts.yml
+++ b/test/integration/targets/gathering_facts/test_gathering_facts.yml
@@ -122,6 +122,24 @@
           - 'ansible_virtualization_role|default("UNDEF_VIRT") == "UNDEF_VIRT"'
           - 'ansible_env|default("UNDEF_ENV") != "UNDEF_ENV"'
 
+- hosts: facthost24
+  tags: [ 'fact_min' ]
+  connection: local
+  gather_facts: no
+  tasks:
+    - setup:
+        filter:
+            - "*env*"
+            - "*virt*"
+
+    - name: Test that retrieving all facts filtered to env and virt works
+      assert:
+        that:
+          - 'ansible_interfaces|default("UNDEF_NET") == "UNDEF_NET"'
+          - 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
+          - 'ansible_virtualization_role|default("UNDEF_VIRT") != "UNDEF_VIRT"'
+          - 'ansible_env|default("UNDEF_ENV") != "UNDEF_ENV"'
+
 - hosts: facthost13
   tags: [ 'fact_min' ]
   connection: local
diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt
index 7ee930142b8..101c769eb9d 100644
--- a/test/sanity/ignore.txt
+++ b/test/sanity/ignore.txt
@@ -147,7 +147,6 @@ lib/ansible/modules/iptables.py pylint:blacklisted-name
 lib/ansible/modules/iptables.py validate-modules:parameter-list-no-elements
 lib/ansible/modules/service.py validate-modules:nonexistent-parameter-documented
 lib/ansible/modules/service.py validate-modules:use-run-command-not-popen
-lib/ansible/modules/setup.py validate-modules:doc-missing-type
 lib/ansible/modules/setup.py validate-modules:parameter-list-no-elements
 lib/ansible/modules/setup.py validate-modules:parameter-type-not-in-doc
 lib/ansible/modules/systemd.py validate-modules:parameter-invalid