Repository: ansible/ansible

Issue:
ansible_facts function is missing after 2.4 facts refactoring
##### ISSUE TYPE

 - Bug Report


##### COMPONENT NAME
lib/ansible/module_utils/facts.py

##### ANSIBLE VERSION

```
2.4
```

##### CONFIGURATION
N/A

##### OS / ENVIRONMENT
N/A

##### SUMMARY
2.3 and below had a function called "ansible_facts" in module_utils/facts.py. After the recent facts refactor that function is gone. Openshift was using that function in a custom module and would probably like for it to remain.

https://github.com/openshift/openshift-ansible/blob/master/roles/openshift_facts/library/openshift_facts.py#L28
https://github.com/openshift/openshift-ansible/blob/master/roles/openshift_facts/library/openshift_facts.py#L1924

##### STEPS TO REPRODUCE
python -c 'from ansible.module_utils.facts import ansible_facts'

##### EXPECTED RESULTS
no import failure

##### ACTUAL RESULTS

```
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ImportError: cannot import name ansible_facts
```


Discussion:
Is the module intentionally re-collecting system facts?  The old ansible_facts() method was more or less equivalent to running the setup module or a regular 'gather_facts: True'.

Is openshift_facts used in lieu of setup.py for fact collection?  Or does it (re)-collect system facts so it can reference them locally? (Like the code in https://github.com/openshift/openshift-ansible/blob/master/roles/openshift_facts/library/openshift_facts.py#L2020-L2026 for example)

Most of ansible_facts() impl was moved into the setup.py module since it is specific to gathering the default facts setup.py is responsible for. I can potentially move it back into ansible.module_utils.facts for compat. I need to figure out which old version of ansible.module_utils.facts I should target for compatibility since the 2.0 and 2.2 apis arent compatible to each other.
@abutcher would know best
> Is the module intentionally re-collecting system facts? The old ansible_facts() method was more or less equivalent to running the setup module or a regular 'gather_facts: True'.

> Is openshift_facts used in lieu of setup.py for fact collection? Or does it (re)-collect system facts so it can reference them locally? (Like the code in https://github.com/openshift/openshift-ansible/blob/master/roles/openshift_facts/library/openshift_facts.py#L2020-L2026 for example)

They're recollected so that we can reference facts like `ansible_default_ipv4`, `ansible_product_name`, `ansible_product_version`, `ansible_pkg_mgr`, `ansible_fqdn`, `ansible_nodename`, `ansible_virtualization_type` and `ansible_virtualization_role` when making configuration decisions within our facts module.

We currently try to call `ansible_facts` and then fallback to `get_all_facts(module)['ansible_facts']` which will also be gone in 2.4.
https://github.com/openshift/openshift-ansible/blob/master/roles/openshift_facts/library/openshift_facts.py#L1931.

I think it makes sense to provide a mechanism to collect these facts but we could probably pass in system facts (collected by setup) when we call our facts module?
CC: @mtnbikenc 

Resolving patch:
diff --git a/lib/ansible/module_utils/facts/__init__.py b/lib/ansible/module_utils/facts/__init__.py
--- a/lib/ansible/module_utils/facts/__init__.py
+++ b/lib/ansible/module_utils/facts/__init__.py
@@ -0,0 +1,21 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+# import from the compat api because 2.0-2.3 had a module_utils.facts.ansible_facts
+# and get_all_facts in top level namespace
+from ansible.module_utils.facts.compat import ansible_facts, get_all_facts   # noqa
diff --git a/lib/ansible/module_utils/facts/compat.py b/lib/ansible/module_utils/facts/compat.py
new file mode 100644
--- /dev/null
+++ b/lib/ansible/module_utils/facts/compat.py
@@ -0,0 +1,75 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.facts.namespace import PrefixFactNamespace
+from ansible.module_utils.facts import default_collectors
+from ansible.module_utils.facts import ansible_collector
+
+
+def get_all_facts(module):
+    '''compat api for ansible 2.2/2.3 module_utils.facts.get_all_facts method
+
+    Expects module to be an instance of AnsibleModule, with a 'gather_subset' param.
+
+    returns a dict mapping the bare fact name ('default_ipv4' with no 'ansible_' namespace) to
+    the fact value.'''
+
+    gather_subset = module.params['gather_subset']
+    return ansible_facts(module, gather_subset=gather_subset)
+
+
+def ansible_facts(module, gather_subset=None):
+    '''Compat api for ansible 2.0/2.2/2.3 module_utils.facts.ansible_facts method
+
+    2.3/2.3 expects a gather_subset arg.
+    2.0/2.1 does not except a gather_subset arg
+
+    So make gather_subsets an optional arg, defaulting to configured DEFAULT_GATHER_TIMEOUT
+
+    'module' should be an instance of an AnsibleModule.
+
+    returns a dict mapping the bare fact name ('default_ipv4' with no 'ansible_' namespace) to
+    the fact value.
+    '''
+
+    gather_subset = gather_subset or module.params.get('gather_subset', ['all'])
+    gather_timeout = module.params.get('gather_timeout', 10)
+    filter_spec = module.params.get('filter', '*')
+
+    minimal_gather_subset = frozenset(['apparmor', 'caps', 'cmdline', 'date_time',
+                                       'distribution', 'dns', 'env', 'fips', 'local', 'lsb',
+                                       'pkg_mgr', 'platform', 'python', 'selinux',
+                                       'service_mgr', 'ssh_pub_keys', 'user'])
+
+    all_collector_classes = default_collectors.collectors
+
+    # don't add a prefix
+    namespace = PrefixFactNamespace(namespace_name='ansible',
+                                    prefix='')
+
+    fact_collector = \
+        ansible_collector.get_ansible_collector(all_collector_classes=all_collector_classes,
+                                                namespace=namespace,
+                                                filter_spec=filter_spec,
+                                                gather_subset=gather_subset,
+                                                gather_timeout=gather_timeout,
+                                                minimal_gather_subset=minimal_gather_subset)
+
+    facts_dict = fact_collector.collect(module=module)
+
+    return facts_dict
diff --git a/lib/ansible/module_utils/facts/default_collectors.py b/lib/ansible/module_utils/facts/default_collectors.py
--- a/lib/ansible/module_utils/facts/default_collectors.py
+++ b/lib/ansible/module_utils/facts/default_collectors.py
@@ -123,7 +123,7 @@
               OpenBSDVirtualCollector,
               NetBSDVirtualCollector,
               SunOSVirtualCollector,
-              HPUXVirtualCollector]
+              HPUXVirtualCollector,
 
-external_collectors = [FacterFactCollector,
-                       OhaiFactCollector]
+              FacterFactCollector,
+              OhaiFactCollector]


Test patch:
diff --git a/test/integration/targets/gathering_facts/test_gathering_facts.yml b/test/integration/targets/gathering_facts/test_gathering_facts.yml
--- a/test/integration/targets/gathering_facts/test_gathering_facts.yml
+++ b/test/integration/targets/gathering_facts/test_gathering_facts.yml
@@ -32,6 +32,7 @@
           - 'ansible_mounts|default("UNDEF_NET") != "UNDEF_HW"'
           - 'ansible_virtualization_role|default("UNDEF_VIRT") != "UNDEF_VIRT"'
 
+
 - hosts: facthost19
   tags: [ 'fact_min' ]
   connection: local
@@ -260,3 +261,18 @@
       assert:
         that:
           - '"{{ ansible_local.testfact.fact_dir }}" == "custom"'
+
+- hosts: facthost20
+  tags: [ 'fact_facter_ohai' ]
+  connection: local
+  gather_subset:
+    - facter
+    - ohai
+  gather_facts: yes
+  tasks:
+    - name: Test that retrieving facter and ohai doesnt fail
+      assert:
+        # not much to assert here, aside from not crashing, since test images dont have
+        # facter/ohai
+        that:
+          - 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
diff --git a/test/units/module_utils/facts/test_ansible_collector.py b/test/units/module_utils/facts/test_ansible_collector.py
--- a/test/units/module_utils/facts/test_ansible_collector.py
+++ b/test/units/module_utils/facts/test_ansible_collector.py
@@ -293,3 +293,21 @@ class TestMinimalCollectedFacts(TestCollectedFacts):
     expected_facts = ['gather_subset',
                       'module_setup']
     not_expected_facts = ['lsb']
+
+
+class TestFacterCollectedFacts(TestCollectedFacts):
+    gather_subset = ['!all', 'facter']
+    min_fact_count = 1
+    max_fact_count = 10
+    expected_facts = ['gather_subset',
+                      'module_setup']
+    not_expected_facts = ['lsb']
+
+
+class TestOhaiCollectedFacts(TestCollectedFacts):
+    gather_subset = ['!all', 'ohai']
+    min_fact_count = 1
+    max_fact_count = 10
+    expected_facts = ['gather_subset',
+                      'module_setup']
+    not_expected_facts = ['lsb']
diff --git a/test/units/module_utils/facts/test_collector.py b/test/units/module_utils/facts/test_collector.py
--- a/test/units/module_utils/facts/test_collector.py
+++ b/test/units/module_utils/facts/test_collector.py
@@ -155,6 +155,30 @@ def test_env(self):
         self.assertIsInstance(res, list)
         self.assertEqual(res, [default_collectors.EnvFactCollector])
 
+    def test_facter(self):
+        res = self._classes(all_collector_classes=default_collectors.collectors,
+                            gather_subset=set(['env', 'facter']))
+        self.assertIsInstance(res, list)
+        self.assertEqual(set(res),
+                         set([default_collectors.EnvFactCollector,
+                              default_collectors.FacterFactCollector]))
+
+    def test_facter_ohai(self):
+        res = self._classes(all_collector_classes=default_collectors.collectors,
+                            gather_subset=set(['env', 'facter', 'ohai']))
+        self.assertIsInstance(res, list)
+        self.assertEqual(set(res),
+                         set([default_collectors.EnvFactCollector,
+                              default_collectors.FacterFactCollector,
+                              default_collectors.OhaiFactCollector]))
+
+    def test_just_facter(self):
+        res = self._classes(all_collector_classes=default_collectors.collectors,
+                            gather_subset=set(['facter']))
+        self.assertIsInstance(res, list)
+        self.assertEqual(set(res),
+                         set([default_collectors.FacterFactCollector]))
+
     def test_collector_specified_multiple_times(self):
         res = self._classes(all_collector_classes=default_collectors.collectors,
                             gather_subset=set(['platform', 'all', 'machine']))