Repository: ansible/ansible

Issue:
case for ansible_distribution and ansible_os_family changes between 2.0.2 and 2.2.0
##### ISSUE TYPE
- Bug Report
##### ANSIBLE VERSION

Old version:

```
ansible 2.0.2.0
  config file = /Users/jladd/.ansible.cfg
  configured module search path = Default w/o overrides
```

New version:

The version below comes from repo `git@github.com:/abadger/ansible.git`, branch `ziploader-namespace`. I used this repo to get a fix for https://github.com/ansible/ansible/pull/15656. The version on this branch shows up as 2.2.0. 

```
ansible 2.2.0 (ziploader-namespace 487e6562ca) last updated 2016/04/29 12:01:05 (GMT -400)
  lib/ansible/modules/core: (detached HEAD e78ee3b128) last updated 2016/04/28 21:43:44 (GMT -400)
  lib/ansible/modules/extras: (detached HEAD ca310f3d15) last updated 2016/04/28 21:43:49 (GMT -400)
  config file = /Users/jladd/.ansible.cfg
  configured module search path = Default w/o overrides
```
##### CONFIGURATION

```
[ssh_connection]
control_path = %(directory)s/%%h-%%r
```
##### OS / ENVIRONMENT

Control host is Mac OS X 10.11.4 (Don't think that part is relevant)
Only target machine I've tested this with is RHEL 7.2
##### SUMMARY

The case for two ansible facts - `ansible_distribution` and `ansible_os_family` - has changed between 2.0.2 and 2.2.0. 

On 2.0.2:
- ansible_distribution: `RedHat`
- ansible_os_family: `RedHat`

On 2.2.0:
- ansible_distribution: `Redhat`
- ansible_os_family: `Redhat`

_Potential Impact:_

Users who have templates with conditionals like:

`{% if ansible_distribution in ['RedHat','CentOS'] and .. %}`

..will find that the above condition which was true in 2.0.2, is no longer true in 2.2.0 because of the case change.
##### STEPS TO REPRODUCE
1. Deployed a RHEL 7.2 instance in ec2.
2. Create an inventory file for the instance:
   `
   ec2-54-92-144-48.compute-1.amazonaws.com ansible_ssh_user=ec2-user
   `
3. With Ansible 2.0.2, run: 
   - `ansible -i playbooks/inventory.log -m setup ec2-54-224-165-31.compute-1.amazonaws.com > facts_2.0.2.txt`
4. With Ansible 2.2.0, run:
   -  `ansible -i playbooks/inventory.log -m setup ec2-54-224-165-31.compute-1.amazonaws.com > facts_2.2.0.txt`
5. Compare two results:
   - `diff -u facts_2.0.2.txt facts_2.2.0.txt`
##### EXPECTED RESULTS

Expect diff to only show trivial differences (e.g. change in date/time, etc).
##### ACTUAL RESULTS

The case for two ansible facts - `ansible_distribution` and `ansible_os_family` - has changed. See Summary section for details.
- Full diff of facts available [here](https://gist.github.com/jladdjr/8f35202b2bd91e0e17796f1272b90023)



Discussion:

Resolving patch:
diff --git a/lib/ansible/module_utils/facts.py b/lib/ansible/module_utils/facts.py
--- a/lib/ansible/module_utils/facts.py
+++ b/lib/ansible/module_utils/facts.py
@@ -605,25 +605,31 @@ class Distribution(object):
     """
 
     OSDIST_LIST = (
-        ('/etc/oracle-release', 'OracleLinux'),
-        ('/etc/slackware-version', 'Slackware'),
-        ('/etc/redhat-release', 'RedHat'),
-        ('/etc/vmware-release', 'VMwareESX'),
-        ('/etc/openwrt_release', 'OpenWrt'),
-        ('/etc/system-release', 'OtherLinux'),
-        ('/etc/alpine-release', 'Alpine'),
-        ('/etc/release', 'Solaris'),
-        ('/etc/arch-release', 'Archlinux'),
-        ('/etc/os-release', 'SuSE'),
-        ('/etc/SuSE-release', 'SuSE'),
-        ('/etc/gentoo-release', 'Gentoo'),
-        ('/etc/os-release', 'Debian'),
-        ('/etc/lsb-release', 'Mandriva'),
-        ('/etc/altlinux-release', 'Altlinux'),
-        ('/etc/os-release', 'NA'),
-        ('/etc/coreos/update.conf', 'Coreos'),
+        {'path': '/etc/oracle-release', 'name': 'OracleLinux'},
+        {'path': '/etc/slackware-version', 'name': 'Slackware'},
+        {'path': '/etc/redhat-release', 'name': 'RedHat'},
+        {'path': '/etc/vmware-release', 'name': 'VMwareESX', 'allowempty': True},
+        {'path': '/etc/openwrt_release', 'name': 'OpenWrt'},
+        {'path': '/etc/system-release', 'name': 'Amazon'},
+        {'path': '/etc/alpine-release', 'name': 'Alpine'},
+        {'path': '/etc/release', 'name': 'Solaris'},
+        {'path': '/etc/arch-release', 'name': 'Archlinux', 'allowempty': True},
+        {'path': '/etc/os-release', 'name': 'SuSE'},
+        {'path': '/etc/SuSE-release', 'name': 'SuSE'},
+        {'path': '/etc/gentoo-release', 'name': 'Gentoo'},
+        {'path': '/etc/os-release', 'name': 'Debian'},
+        {'path': '/etc/lsb-release', 'name': 'Mandriva'},
+        {'path': '/etc/altlinux-release', 'name': 'Altlinux'},
+        {'path': '/etc/os-release', 'name': 'NA'},
+        {'path': '/etc/coreos/update.conf', 'name': 'Coreos'},
     )
 
+    SEARCH_STRING = {
+        'OracleLinux': 'Oracle Linux',
+        'RedHat': 'Red Hat',
+        'Altlinux': 'ALT Linux',
+    }
+
     # A list with OS Family members
     OS_FAMILY = dict(
         RedHat = 'RedHat', Fedora = 'RedHat', CentOS = 'RedHat', Scientific = 'RedHat',
@@ -675,36 +681,47 @@ def get_distribution_facts(self):
             self.facts['distribution_release'] = dist[2] or 'NA'
             # Try to handle the exceptions now ...
             # self.facts['distribution_debug'] = []
-            for (path, name) in self.OSDIST_LIST:
+            for ddict in self.OSDIST_LIST:
+                name = ddict['name']
+                path = ddict['path']
 
-                # TODO: remove this hack if tested with Fedora and Altlinux
-                if self.facts['distribution'] in ('Fedora', 'Altlinux', ):
-                    # Once we determine the value is one of these distros
-                    # we trust the values are always correct
-                    break
                 if not os.path.exists(path):
                     continue
                 if os.path.getsize(path) == 0:
-                    continue
+                    if 'allowempty' in ddict and ddict['allowempty']:
+                        self.facts['distribution'] = name
+                        break
+                    else:
+                        continue
 
-                try:
+                data = get_file_content(path)
+                if name in self.SEARCH_STRING:
+                    # look for the distribution string in the data and replace according to RELEASE_NAME_MAP
+                    # only the distribution name is set, the version is assumed to be correct from platform.dist()
+                    if self.SEARCH_STRING[name] in data:
+                        # this sets distribution=RedHat if 'Red Hat' shows up in data
+                        self.facts['distribution'] = name
+                    else:
+                        # this sets distribution to what's in the data, e.g. CentOS, Scientific, ...
+                        self.facts['distribution'] = data.split()[0]
+                    break
+                else:
+                    # call a dedicated function for parsing the file content
                     distfunc = getattr(self, 'get_distribution_' + name)
-                    data = get_file_content(path)
                     parsed = distfunc(name, data, path)
                     if parsed is None or parsed:
+                        # distfunc return False if parsing failed
                         # break only if parsing was succesful
                         # otherwise continue with other distributions
-
-                        # to debug multiple matching release files, one can use:
-                        # self.facts['distribution_debug'].append({path + ' ' + name:
-                        #         (parsed,
-                        #          self.facts['distribution'],
-                        #          self.facts['distribution_version'],
-                        #          self.facts['distribution_release'],
-                        #          )})
                         break
-                except AttributeError:
-                    pass
+
+                    # to debug multiple matching release files, one can use:
+                    # self.facts['distribution_debug'].append({path + ' ' + name:
+                    #         (parsed,
+                    #          self.facts['distribution'],
+                    #          self.facts['distribution_version'],
+                    #          self.facts['distribution_release'],
+                    #          )})
 
         self.facts['os_family'] = self.facts['distribution']
         distro = self.facts['distribution'].replace(' ', '_')
@@ -738,47 +755,30 @@ def get_distribution_OpenBSD(self):
         else:
             self.facts['distribution_version'] = 'release'
 
-    def get_distribution_Archlinux(self, name, data, path):
-        if not 'Arch Linux' in data:
-            self.facts['distribution'] = data.split()[0]
-
     def get_distribution_Slackware(self, name, data, path):
-        if 'Slackware' in data:
-            self.facts['distribution'] = name
-            version = re.findall('\w+[.]\w+', data)
-            if version:
-                self.facts['distribution_version'] = version[0]
-
-    def get_distribution_OracleLinux(self, name, data, path):
-        if not 'Oracle Linux' in data:
-            self.facts['distribution'] = data.split()[0]
-
-    def get_distribution_RedHat(self, name, data, path):
-        if not 'Red Hat' in data:
-            self.facts['distribution'] = data.split()[0]
-
-    def get_distribution_Altlinux(self, name, data, path):
-        if not 'ALT Linux' in data:
-            self.facts['distribution'] = data.split()[0]
-
-    def get_distribution_OtherLinux(self, name, data, path):
-        if 'Amazon' in data:
-            self.facts['distribution'] = 'Amazon'
-            self.facts['distribution_version'] = data.split()[-1]
-        else:
-            return False  # TODO: remove if tested without this
+        if 'Slackware' not in data:
+            return False  # TODO: remove
+        self.facts['distribution'] = name
+        version = re.findall('\w+[.]\w+', data)
+        if version:
+            self.facts['distribution_version'] = version[0]
+
+    def get_distribution_Amazon(self, name, data, path):
+        if 'Amazon' not in data:
+            return False  # TODO: remove
+        self.facts['distribution'] = 'Amazon'
+        self.facts['distribution_version'] = data.split()[-1]
 
     def get_distribution_OpenWrt(self, name, data, path):
-        if 'OpenWrt' in data:
-            self.facts['distribution'] = name
-            version = re.search('DISTRIB_RELEASE="(.*)"', data)
-            if version:
-                self.facts['distribution_version'] = version.groups()[0]
-            release = re.search('DISTRIB_CODENAME="(.*)"', data)
-            if release:
-                self.facts['distribution_release'] = release.groups()[0]
-        else:
-            return False  # TODO: remove if tested without this
+        if 'OpenWrt' not in data:
+            return False  # TODO: remove
+        self.facts['distribution'] = name
+        version = re.search('DISTRIB_RELEASE="(.*)"', data)
+        if version:
+            self.facts['distribution_version'] = version.groups()[0]
+        release = re.search('DISTRIB_CODENAME="(.*)"', data)
+        if release:
+            self.facts['distribution_release'] = release.groups()[0]
 
     def get_distribution_Alpine(self, name, data, path):
         self.facts['distribution'] = 'Alpine'


Test patch:
diff --git a/hacking/tests/gen_distribution_version_testcase.py b/hacking/tests/gen_distribution_version_testcase.py
--- a/hacking/tests/gen_distribution_version_testcase.py
+++ b/hacking/tests/gen_distribution_version_testcase.py
@@ -46,11 +46,14 @@
 
 
 facts = ['distribution', 'distribution_version', 'distribution_release', 'distribution_major_version']
-ansible_out = subprocess.check_output(['ansible', 'localhost', '-m', 'setup'])
+ansible_out = subprocess.Popen(['ansible', 'localhost', '-m', 'setup'], stdout=subprocess.PIPE).communicate()[0]
 parsed = json.loads(ansible_out[ansible_out.index('{'):])
 ansible_facts = {}
 for fact in facts:
-    ansible_facts[fact] = parsed['ansible_facts']['ansible_'+fact]
+    try:
+        ansible_facts[fact] = parsed['ansible_facts']['ansible_'+fact]
+    except:
+        ansible_facts[fact] = "N/A"
 
 nicename = ansible_facts['distribution'] + ' ' + ansible_facts['distribution_version']
 
diff --git a/test/units/module_utils/test_distribution_version.py b/test/units/module_utils/test_distribution_version.py
--- a/test/units/module_utils/test_distribution_version.py
+++ b/test/units/module_utils/test_distribution_version.py
@@ -22,9 +22,6 @@
 
 # to work around basic.py reading stdin
 import json
-from io import BytesIO, StringIO
-from ansible.compat.six import PY3
-from ansible.utils.unicode import to_bytes
 
 from units.mock.procenv import swap_stdin_and_argv
 
@@ -38,6 +35,82 @@
 # to generate the testcase data, you can use the script gen_distribution_version_testcase.py in hacking/tests
 TESTSETS = [
     {
+    "platform.dist": [
+        "centos",
+        "7.2.1511",
+        "Core"
+    ],
+    "input": {
+        "/etc/redhat-release": "CentOS Linux release 7.2.1511 (Core) \n",
+        "/etc/os-release": "NAME=\"CentOS Linux\"\nVERSION=\"7 (Core)\"\nID=\"centos\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"7\"\nPRETTY_NAME=\"CentOS Linux 7 (Core)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:centos:centos:7\"\nHOME_URL=\"https://www.centos.org/\"\nBUG_REPORT_URL=\"https://bugs.centos.org/\"\n\nCENTOS_MANTISBT_PROJECT=\"CentOS-7\"\nCENTOS_MANTISBT_PROJECT_VERSION=\"7\"\nREDHAT_SUPPORT_PRODUCT=\"centos\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"7\"\n\n",
+        "/etc/system-release": "CentOS Linux release 7.2.1511 (Core) \n"
+    },
+    "name": "CentOS 7.2.1511",
+    "result": {
+        "distribution_release": "Core",
+        "distribution": "CentOS",
+        "distribution_major_version": "7",
+        "distribution_version": "7.2.1511"
+    }
+},
+    {
+    "name": "CentOS 6.7",
+    "platform.dist": [
+        "centos",
+        "6.7",
+        "Final"
+    ],
+    "input": {
+        "/etc/redhat-release": "CentOS release 6.7 (Final)\n",
+        "/etc/lsb-release": "LSB_VERSION=base-4.0-amd64:base-4.0-noarch:core-4.0-amd64:core-4.0-noarch:graphics-4.0-amd64:graphics-4.0-noarch:printing-4.0-amd64:printing-4.0-noarch\n",
+        "/etc/system-release": "CentOS release 6.7 (Final)\n"
+    },
+    "result": {
+        "distribution_release": "Final",
+        "distribution": "CentOS",
+        "distribution_major_version": "6",
+        "distribution_version": "6.7"
+    }
+},
+    {
+    "name": "RedHat 7.2",
+    "platform.dist": [
+        "redhat",
+        "7.2",
+        "Maipo"
+    ],
+    "input": {
+        "/etc/redhat-release": "Red Hat Enterprise Linux Server release 7.2 (Maipo)\n",
+        "/etc/os-release": "NAME=\"Red Hat Enterprise Linux Server\"\nVERSION=\"7.2 (Maipo)\"\nID=\"rhel\"\nID_LIKE=\"fedora\"\nVERSION_ID=\"7.2\"\nPRETTY_NAME=\"Red Hat Enterprise Linux Server 7.2 (Maipo)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:redhat:enterprise_linux:7.2:GA:server\"\nHOME_URL=\"https://www.redhat.com/\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\n\nREDHAT_BUGZILLA_PRODUCT=\"Red Hat Enterprise Linux 7\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=7.2\nREDHAT_SUPPORT_PRODUCT=\"Red Hat Enterprise Linux\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"7.2\"\n",
+        "/etc/system-release": "Red Hat Enterprise Linux Server release 7.2 (Maipo)\n"
+    },
+    "result": {
+        "distribution_release": "Maipo",
+        "distribution": "RedHat",
+        "distribution_major_version": "7",
+        "distribution_version": "7.2"
+    }
+},
+{
+    "name": "RedHat 6.7",
+    "platform.dist": [
+        "redhat",
+        "6.7",
+        "Santiago"
+    ],
+    "input": {
+        "/etc/redhat-release": "Red Hat Enterprise Linux Server release 6.7 (Santiago)\n",
+        "/etc/lsb-release": "LSB_VERSION=base-4.0-amd64:base-4.0-noarch:core-4.0-amd64:core-4.0-noarch:graphics-4.0-amd64:graphics-4.0-noarch:printing-4.0-amd64:printing-4.0-noarch\n",
+        "/etc/system-release": "Red Hat Enterprise Linux Server release 6.7 (Santiago)\n"
+    },
+    "result": {
+        "distribution_release": "Santiago",
+        "distribution": "RedHat",
+        "distribution_major_version": "6",
+        "distribution_version": "6.7"
+    }
+},
+{
         "name" : "openSUSE Leap 42.1",
         "input": {
             "/etc/os-release":
@@ -339,7 +412,7 @@ def test_distribution_version():
 
 def _test_one_distribution(facts, module, testcase):
     """run the test on one distribution testcase
-    
+
     * prepare some mock functions to get the testdata in
     * run Facts()
     * compare with the expected output
@@ -373,7 +446,7 @@ def mock_path_getsize(fname):
     @patch('platform.system', lambda: 'Linux')
     def get_facts(testcase):
         return facts.Facts(module).populate()
-    
+
     generated_facts = get_facts(testcase)
 
     # testcase['result'] has a list of variables and values it expects Facts() to set