Repository: ansible/ansible

Issue:
package module fails to install/remove packages on ALTLinux
<!---
Verify first that your issue/request is not already reported on GitHub.
THIS FORM WILL BE READ BY A MACHINE, COMPLETE ALL SECTIONS AS DESCRIBED.
Also test if the latest release, and devel branch are affected too.
ALWAYS add information AFTER (OUTSIDE) these html comments.
Otherwise it may end up being automatically closed by our bot. -->

##### SUMMARY
<!--- Explain the problem briefly -->
`package` module fails to install/remove packages on [ALTLinux](https://en.altlinux.org/Main_Page).

##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
<!--- Insert, BELOW THIS COMMENT, the name of the module, plugin, task or feature.
Do not include extra details here, e.g. "vyos_command" not "the network module vyos_command" or the full path-->

##### ANSIBLE VERSION
<!--- Paste, BELOW THIS COMMENT, verbatim output from "ansible --version" between quotes below -->
```
ansible 2.5.2
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/asheplyakov/.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.11 (default, May 30 2018, 12:09:46) [GCC 5.3.1 20151207 (ALT Linux 5.3.1-alt3)]

```

##### CONFIGURATION
<!--- If using Ansible 2.4 or above, paste, BELOW THIS COMMENT, the results of "ansible-config dump --only-changed"
Otherwise, mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).-->

##### OS / ENVIRONMENT
<!--- Mention, BELOW THIS COMMENT, the OS you are running Ansible from, and the OS you are
managing, or say "N/A" for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.-->

ALTLinux 8.1 (control node), ALTLinux 8.1 (managed node(s))

##### STEPS TO REPRODUCE
<!--- For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used. -->

<!--- Paste example playbooks or commands between quotes below -->
$ cat pkgbug.yml
```yaml
---
- hosts:
  - localhost
  tasks:
  - name: install gcc
    package: name=gcc5 state=present
  become:true

```
$ ansible-playbook  pkgbug.yml

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->

ansible installs the `gcc5` package (or does nothing if package is already installed)

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
 [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'


PLAY [localhost] ***********************************************************************************************************************************************************

TASK [Gathering Facts] *****************************************************************************************************************************************************
ok: [localhost]

TASK [install gcc] *********************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "cmd": "apt-get install python-apt -y -q", "msg": "E: Couldn't find package python-apt", "rc": 100, "stderr": "E: Couldn't find package python-apt\n", "stderr_lines": ["E: Couldn't find package python-apt"], "stdout": "Reading Package Lists...\nBuilding Dependency Tree...\n", "stdout_lines": ["Reading Package Lists...", "Building Dependency Tree..."]}
	to retry, use: --limit @/home/DOMAIN.ALT/asheplyakov/pkgbug.retry

PLAY RECAP *****************************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1   

```

ALTLinux uses [APT-RPM](https://en.wikipedia.org/wiki/APT-RPM), however it looks like ansible is not aware of this, and tries to use `apt` module instead of `apt_rpm`.


Discussion:
The `package` action relies on fact gathering to know the package manager, please check with 2.6 or devel as i believe we have updated it to work correctly on ALTLinux

The `apt_rpm` module does seem to exist, you can use this directly or instruct `package` via `use=apt_rpm`
@bcoca 

> please check with 2.6 or devel as i believe we have updated it to work correctly on ALTLinux

devel still tries to use `apt` as a package manager:

`$ ansible -m setup localhost 2>/dev/null |grep ansible_pkg_mgr
                "ansible_pkg_mgr": "apt",`


[PkgMgrFactCollector](https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/facts/system/pkg_mgr.py#L107) checks for `Altlinux` `ansible_os_family`, however the actual value of `ansible_os_family` seems to be `ALT`:

`ansible -m setup localhost 2>/dev/null| grep ansible_os_family
        "ansible_os_family": "ALT",`

For some reason recent versions of ALTLinux ship the /etc/redhat-release symlink (which points to /etc/altlinux-release):

$ cat /etc/altlinux-release
  ALT Workstation K 8.2  (Centaurea Ruthenica)

 [DistributionFiles](https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/facts/system/distribution.py#L52) checks for /etc/redhat-release first (for it's listed earlier in the `OSDIST_LIST`), successfully parses the content, and gathers the following facts:

"ansible_distribution": "ALT", 
"ansible_distribution_file_parsed": true, 
"ansible_distribution_file_path": "/etc/redhat-release", 
"ansible_distribution_file_variety": "RedHat", 
"ansible_distribution_major_version": "8", 
"ansible_distribution_release": "altlinux", 
"ansible_distribution_version": "8.2",

The `ALT` distribution string is the 1st word in the 1st line of that file (it gets assigned  [here](https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/facts/system/distribution.py#L122))

It's possible to fix the problem by moving /etc/altlinux-release into the beginning of [OSDIST_LIST](https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/facts/system/distribution.py#L59), so ansible will check for /etc/altlinux-release before trying /etc/redhat-release. However this might break existing roles/playbooks, since many of them check for `ansible_os_family == 'ALT'` ([example](https://git.postgrespro.ru/a.lakhin/pg-tests/blob/e938b9aa1cb3bdeb1c2f7f045ab3c4c2aeb2d00a/tests_install/playbook-prepare-env.yml#L132))

i did that change, tests seem to be ok with it, but i need to download alt and do more corner cases

Resolving patch:
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
@@ -19,6 +19,7 @@
 __metaclass__ = type
 
 import os
+import subprocess
 
 from ansible.module_utils.facts.collector import BaseFactCollector
 
@@ -89,6 +90,23 @@ def _check_rh_versions(self, pkg_mgr_name, collected_facts):
                 pkg_mgr_name = 'dnf'
         return pkg_mgr_name
 
+    def _check_apt_flavor(self, pkg_mgr_name):
+        # Check if '/usr/bin/apt' is APT-RPM or an ordinary (dpkg-based) APT.
+        # There's rpm package on Debian, so checking if /usr/bin/rpm exists
+        # is not enough. Instead ask RPM if /usr/bin/apt-get belongs to some
+        # RPM package.
+        rpm_query = '/usr/bin/rpm -q --whatprovides /usr/bin/apt-get'.split()
+        if os.path.exists('/usr/bin/rpm'):
+            with open(os.devnull, 'w') as null:
+                try:
+                    subprocess.check_call(rpm_query, stdout=null, stderr=null)
+                    pkg_mgr_name = 'apt_rpm'
+                except subprocess.CalledProcessError:
+                    # No apt-get in RPM database. Looks like Debian/Ubuntu
+                    # with rpm package installed
+                    pkg_mgr_name = 'apt'
+        return pkg_mgr_name
+
     def collect(self, module=None, collected_facts=None):
         facts_dict = {}
         collected_facts = collected_facts or {}
@@ -108,6 +126,10 @@ def collect(self, module=None, collected_facts=None):
             if pkg_mgr_name == 'apt':
                 pkg_mgr_name = 'apt_rpm'
 
+        # Check if /usr/bin/apt-get is ordinary (dpkg-based) APT or APT-RPM
+        if pkg_mgr_name == 'apt':
+            pkg_mgr_name = self._check_apt_flavor(pkg_mgr_name)
+
         # pacman has become available by distros other than those that are Arch
         # based by virtue of a dependency to the systemd mkosi project, this
         # handles some of those scenarios as they are reported/requested


Test patch: