Repository: ansible/ansible-modules-core

Issue:
yum repo not showing failed on a package not found
<!--- Verify first that your issue/request is not already reported in GitHub -->
##### ISSUE TYPE

<!--- Pick one below and delete the rest: -->
- Bug Report
##### COMPONENT NAME

<!--- Name of the plugin/module/task -->

yum module
##### ANSIBLE VERSION

<!--- Paste verbatim output from “ansible --version” between quotes below -->

```
ansible 2.1.0.0
  config file = 
  configured module search path = Default w/o overrides

```
##### CONFIGURATION

standard, nothing off the wall
##### OS / ENVIRONMENT

CentOS 7
##### SUMMARY

Output shows `ok` but the task actually failed, very confusing
##### STEPS TO REPRODUCE

<!---
For bugs, show exactly how to reproduce the problem.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->

```
- name: install some repos
  become: yes
  yum:
    name: "{{ item }}"
    state: latest
  with_items:
    - epel-release
    - docker-io

- name: Install pip, python-dev package and docker with yum
  become: yes
  yum:
    pkg: "{{ item }}"
    state: latest
  with_items:
    - python-dev
    - python-pip
    - docker

- name: Install pip modules
  become: yes
  pip:
    name: "{{ item.name }}"
    version: "{{ item.version | default(omit) }}"
  with_items:
    - name: docker-py
      version: 1.8.1
    - name: docker-compose

- name: enable docker
  become: yes
  service:
    name: docker
    state: started
    enabled: yes

```
##### EXPECTED RESULTS

I would expect the task to fail, python-dev doesn't exist in centos 7
##### ACTUAL RESULTS

```
ok: [xxx.xxx.xxx.xxx] => (item=[u'python-dev', u'python-pip', u'docker']) => {"changed": false, "failed": true, "invocation": {"module_args": {"conf_file": null, "disable_gpg_check": false, "disablerepo": null, "enablerepo": null, "exclude": null, "install_repoquery": true, "list": null, "name": ["python-dev", "python-pip", "docker"], "state": "latest", "update_cache": false, "validate_certs": true}, "module_name": "yum"}, "item": ["python-dev", "python-pip", "docker"], "msg": "No Package matching 'python-dev' found available, installed or updated", "rc": 0, "results": []}
```



Discussion:
Minimal test case:

```
- hosts: localhost
  connection: local
  become: yes

  tasks:
  - name: install stuff
    yum:
      name: "{{ item }}"
      state: latest
    with_items:
    - does-not-exist
```

Note that this fails as expected when `with_items` is not used. 

I don't think this is a fault in the yum module necessarily - it's failing correctly - but in the handling of result of the yum module in combination with with_items. However, I suggest keeping this open here until that's sufficiently confirmed that we can move this to ansible/ansible.


Resolving patch:
diff --git a/packaging/os/yum.py b/packaging/os/yum.py
--- a/packaging/os/yum.py
+++ b/packaging/os/yum.py
@@ -596,7 +596,9 @@ def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
         if spec.endswith('.rpm') and '://' not in spec:
             # get the pkg name-v-r.arch
             if not os.path.exists(spec):
-                res['msg'] += "No Package file matching '%s' found on system" % spec
+                res['msg'] += "No RPM file matching '%s' found on system" % spec
+                res['results'].append("No RPM file matching '%s' found on system" % spec)
+                res['rc'] = 127 # Ensure the task fails in with-loop
                 module.fail_json(**res)
 
             pkg_name = local_name(module, spec)
@@ -632,11 +634,13 @@ def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
                 if installed_pkgs:
                     res['results'].append('%s providing %s is already installed' % (installed_pkgs[0], spec))
                     continue
-            
+
             # look up what pkgs provide this
             pkglist = what_provides(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos)
             if not pkglist:
-                res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
+                res['msg'] += "No package matching '%s' found available, installed or updated" % spec
+                res['results'].append("No package matching '%s' found available, installed or updated" % spec)
+                res['rc'] = 126 # Ensure the task fails in with-loop
                 module.fail_json(**res)
 
             # if any of the packages are involved in a transaction, fail now
@@ -644,6 +648,7 @@ def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
             conflicts = transaction_exists(pkglist)
             if len(conflicts) > 0:
                 res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts)
+                res['rc'] = 125 # Ensure the task fails in with-loop
                 module.fail_json(**res)
 
             # if any of them are installed
@@ -698,8 +703,7 @@ def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
             for spec in items:
                 # Fail on invalid urls:
                 if ('://' in spec and ('No package %s available.' % spec in out or 'Cannot open: %s. Skipping.' % spec in err)):
-                    err = 'Package at %s could not be installed' % spec
-                    module.fail_json(changed=False,msg=err,rc=1)
+                    module.fail_json(msg='Package at %s could not be installed' % spec, rc=1, changed=False)
         if (rc != 0 and 'Nothing to do' in err) or 'Nothing to do' in out:
             # avoid failing in the 'Nothing To Do' case
             # this may happen with an URL spec.
@@ -771,7 +775,7 @@ def remove(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
         # of the process
 
         # at this point we should check to see if the pkg is no longer present
-        
+
         for pkg in pkgs:
             if not pkg.startswith('@'): # we can't sensibly check for a group being uninstalled reliably
                 # look to see if the pkg shows up from is_installed. If it doesn't
@@ -847,7 +851,9 @@ def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
             pkglist = what_provides(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos)
             # FIXME..? may not be desirable to throw an exception here if a single package is missing
             if not pkglist:
-                res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
+                res['msg'] += "No package matching '%s' found available, installed or updated" % spec
+                res['results'].append("No package matching '%s' found available, installed or updated" % spec)
+                res['rc'] = 126 # Ensure the task fails in with-loop
                 module.fail_json(**res)
 
             nothing_to_do = True
@@ -879,6 +885,8 @@ def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
             conflicts = transaction_exists(pkglist)
             if len(conflicts) > 0:
                 res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts)
+                res['results'].append("The following packages have pending transactions: %s" % ", ".join(conflicts))
+                res['rc'] = 128 # Ensure the task fails in with-loop
                 module.fail_json(**res)
 
     # check_mode output


Test patch: