Repository: ansible/ansible

Issue:
ansible_pkg_mgr is "unknown" on OpenBSD since 2.4
Hi,

##### ISSUE TYPE

 - Bug Report

##### COMPONENT NAME

Facts

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.4.0.0
  config file = /home/stephane/.ansible.cfg
  configured module search path = [u'/home/stephane/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.13 (default, Jul 21 2017, 03:24:34) [GCC 7.1.1 20170630]
```

##### CONFIGURATION

```
ANSIBLE_SSH_CONTROL_PATH(/home/stephane/.ansible.cfg) = %(directory)s/%%h-%%r
ANSIBLE_SSH_PIPELINING(/home/stephane/.ansible.cfg) = True
CACHE_PLUGIN(/home/stephane/.ansible.cfg) = jsonfile
CACHE_PLUGIN_CONNECTION(/home/stephane/.ansible.cfg) = ~/.cache/ansible/
CACHE_PLUGIN_TIMEOUT(/home/stephane/.ansible.cfg) = 39600
DEFAULT_GATHERING(/home/stephane/.ansible.cfg) = smart
DEFAULT_HOST_LIST(/home/stephane/.ansible.cfg) = [u'/home/stephane/ansible/inventory']
DEFAULT_LOG_PATH(/home/stephane/.ansible.cfg) = /tmp/ansible.log
DEFAULT_ROLES_PATH(/home/stephane/.ansible.cfg) = [u'/home/stephane/ansible/roles']
DEFAULT_SQUASH_ACTIONS(/home/stephane/.ansible.cfg) = ['apk', 'apt', 'dnf', 'homebrew', 'pacman', 'pkgng', 'yum', 'zypper']
DEFAULT_VAULT_PASSWORD_FILE(/home/stephane/.ansible.cfg) = /home/stephane/ansible/scripts/get_vault_password.sh
RETRY_FILES_ENABLED(/home/stephane/.ansible.cfg) = False
```

##### OS / ENVIRONMENT

Ansible running from ArchLinux, managing an OpenBSD machine.

##### SUMMARY

Since Ansible 2.4, the `ansible_pkg_mgr` fact is set to `unknown`, instead of `openbsd_pkg` (<= Ansible 2.3.2.0). As the `package` module uses this variable to choose the package manager to invoke, it became unusable on OpenBSD.

##### STEPS TO REPRODUCE

Execute the following command with an OpenBSD machine as a target:
```
ansible openbsd_machine -m setup -a "filter=ansible_pkg_mgr"
```

##### EXPECTED RESULTS

```
openbsd_machine | SUCCESS => {
    "ansible_facts": {
        "ansible_pkg_mgr": "openbsd_pkg"
    }, 
    "changed": false
}
```

##### ACTUAL RESULTS

```
openbsd_machine | SUCCESS => {
    "ansible_facts": {
        "ansible_pkg_mgr": "unknown"
    }, 
    "changed": false, 
    "failed": false
}
```
Let me know if I can be of any help concerning this issue.

Regards


Discussion:
cc @alikins 
@sparunakian What does the 'ansible_system' fact end up being set to?  The pkg_mgr fact collection code has a special case for ansible_system='OpenBSD' to set pkg_mgr='openbsd_pkg'

If you can provide the full output of 'ansible openbsd_machine -m setup.py' (all of the facts without filters) that may be useful.

When you use the 'package' module, how does it fail? If possible, please provide a copy of the output from running with verbosity set to '-vvv' or higher (either 'ansible' or 'ansible-playbook' against a host showing the problem, whichever is easier)
My suspicion is this is related to the order the facts are collected in, and that 'ansible_system' may not be known when the pkg_mgr fact is collected. There is currently no explicit partial ordering for the facts collection so the ordering may be random which could cause the 'ansible_system' to be unknown when pkg_mgr is determined. If thats the case, I need to go ahead and implement some ordering to the collectors to generally fix this.
also dependencies, since now you can avoid collecting 'platform' information

Resolving patch:
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
@@ -33,6 +33,7 @@
 from ansible.module_utils.facts.system.local import LocalFactCollector
 from ansible.module_utils.facts.system.lsb import LSBFactCollector
 from ansible.module_utils.facts.system.pkg_mgr import PkgMgrFactCollector
+from ansible.module_utils.facts.system.pkg_mgr import OpenBSDPkgMgrFactCollector
 from ansible.module_utils.facts.system.platform import PlatformFactCollector
 from ansible.module_utils.facts.system.python import PythonFactCollector
 from ansible.module_utils.facts.system.selinux import SelinuxFactCollector
@@ -110,6 +111,7 @@
               SunOSNetworkCollector,
 
               PkgMgrFactCollector,
+              OpenBSDPkgMgrFactCollector,
               PlatformFactCollector,
               PythonFactCollector,
               SelinuxFactCollector,
diff --git a/lib/ansible/module_utils/facts/system/pkg_mgr.py b/lib/ansible/module_utils/facts/system/pkg_mgr.py
--- a/lib/ansible/module_utils/facts/system/pkg_mgr.py
+++ b/lib/ansible/module_utils/facts/system/pkg_mgr.py
@@ -50,20 +50,28 @@
             ]
 
 
+class OpenBSDPkgMgrFactCollector(BaseFactCollector):
+    name = 'pkg_mgr'
+    _fact_ids = set()
+    _platform = 'OpenBSD'
+
+    def collect(self, module=None, collected_facts=None):
+        facts_dict = {}
+
+        facts_dict['pkg_mgr'] = 'openbsd_pkg'
+        return facts_dict
+
+
 # the fact ends up being 'pkg_mgr' so stick with that naming/spelling
 class PkgMgrFactCollector(BaseFactCollector):
     name = 'pkg_mgr'
     _fact_ids = set()
+    _platform = 'Generic'
 
     def collect(self, module=None, collected_facts=None):
         facts_dict = {}
         collected_facts = collected_facts or {}
 
-        pkg_mgr_name = None
-        if collected_facts.get('system') == 'OpenBSD':
-            facts_dict['pkg_mgr'] = 'openbsd_pkg'
-            return facts_dict
-
         pkg_mgr_name = 'unknown'
         for pkg in PKG_MGRS:
             if os.path.exists(pkg['path']):


Test patch:
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
@@ -1,4 +1,3 @@
-# unit tests for ansible/module_utils/facts/__init__.py
 # -*- coding: utf-8 -*-
 #
 #
@@ -22,7 +21,7 @@
 
 # for testing
 from ansible.compat.tests import unittest
-from ansible.compat.tests.mock import Mock
+from ansible.compat.tests.mock import Mock, patch
 
 from ansible.module_utils.facts import collector
 from ansible.module_utils.facts import ansible_collector
@@ -40,7 +39,7 @@
 from ansible.module_utils.facts.system.fips import FipsFactCollector
 from ansible.module_utils.facts.system.local import LocalFactCollector
 from ansible.module_utils.facts.system.lsb import LSBFactCollector
-from ansible.module_utils.facts.system.pkg_mgr import PkgMgrFactCollector
+from ansible.module_utils.facts.system.pkg_mgr import PkgMgrFactCollector, OpenBSDPkgMgrFactCollector
 from ansible.module_utils.facts.system.platform import PlatformFactCollector
 from ansible.module_utils.facts.system.python import PythonFactCollector
 from ansible.module_utils.facts.system.selinux import SelinuxFactCollector
@@ -59,6 +58,7 @@
      SystemCapabilitiesFactCollector,
      FipsFactCollector,
      PkgMgrFactCollector,
+     OpenBSDPkgMgrFactCollector,
      ServiceMgrFactCollector,
      LSBFactCollector,
      DateTimeFactCollector,
@@ -311,3 +311,34 @@ class TestOhaiCollectedFacts(TestCollectedFacts):
     expected_facts = ['gather_subset',
                       'module_setup']
     not_expected_facts = ['lsb']
+
+
+class TestPkgMgrFacts(TestCollectedFacts):
+    gather_subset = ['pkg_mgr']
+    min_fact_count = 1
+    max_fact_count = 10
+    expected_facts = ['gather_subset',
+                      'module_setup',
+                      'pkg_mgr']
+
+
+class TestOpenBSDPkgMgrFacts(TestPkgMgrFacts):
+    def test_is_openbsd_pkg(self):
+        self.assertIn('pkg_mgr', self.facts)
+        self.assertEqual(self.facts['pkg_mgr'], 'openbsd_pkg')
+
+    def setUp(self):
+        self.patcher = patch('platform.system')
+        mock_platform = self.patcher.start()
+        mock_platform.return_value = 'OpenBSD'
+
+        mock_module = self._mock_module()
+        collectors = self._collectors(mock_module)
+
+        fact_collector = \
+            ansible_collector.AnsibleFactCollector(collectors=collectors,
+                                                   namespace=ns)
+        self.facts = fact_collector.collect(module=mock_module)
+
+    def tearDown(self):
+        self.patcher.stop()
diff --git a/test/units/module_utils/facts/test_collectors.py b/test/units/module_utils/facts/test_collectors.py
--- a/test/units/module_utils/facts/test_collectors.py
+++ b/test/units/module_utils/facts/test_collectors.py
@@ -32,7 +32,7 @@
 from ansible.module_utils.facts.system.dns import DnsFactCollector
 from ansible.module_utils.facts.system.env import EnvFactCollector
 from ansible.module_utils.facts.system.fips import FipsFactCollector
-from ansible.module_utils.facts.system.pkg_mgr import PkgMgrFactCollector
+from ansible.module_utils.facts.system.pkg_mgr import PkgMgrFactCollector, OpenBSDPkgMgrFactCollector
 from ansible.module_utils.facts.system.platform import PlatformFactCollector
 from ansible.module_utils.facts.system.python import PythonFactCollector
 from ansible.module_utils.facts.system.selinux import SelinuxFactCollector
@@ -225,6 +225,29 @@ class TestPkgMgrFacts(BaseFactsTest):
     fact_namespace = 'ansible_pkgmgr'
     collector_class = PkgMgrFactCollector
 
+    def test_collect(self):
+        module = self._mock_module()
+        fact_collector = self.collector_class()
+        facts_dict = fact_collector.collect(module=module, collected_facts=self.collected_facts)
+        self.assertIsInstance(facts_dict, dict)
+        self.assertIn('pkg_mgr', facts_dict)
+
+
+class TestOpenBSDPkgMgrFacts(BaseFactsTest):
+    __test__ = True
+    gather_subset = ['!all', 'pkg_mgr']
+    valid_subsets = ['pkg_mgr']
+    fact_namespace = 'ansible_pkgmgr'
+    collector_class = OpenBSDPkgMgrFactCollector
+
+    def test_collect(self):
+        module = self._mock_module()
+        fact_collector = self.collector_class()
+        facts_dict = fact_collector.collect(module=module, collected_facts=self.collected_facts)
+        self.assertIsInstance(facts_dict, dict)
+        self.assertIn('pkg_mgr', facts_dict)
+        self.assertEqual(facts_dict['pkg_mgr'], 'openbsd_pkg')
+
 
 class TestPlatformFactCollector(BaseFactsTest):
     __test__ = True