Repository: ansible/ansible

Issue:
unarchive and fetch action plugins report bogus error on failure
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
action_plugins

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
v2.2 and earlier

##### SUMMARY
<!--- Explain the problem briefly -->
Internal action-plugin functions that call remote modules internally report bogus errors on failure

##### 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 -->
Consider the following playbook
```yaml
- hosts: localhost
  gather_facts: no
  tasks:
  - unarchive:
      src: test126.tar.bz2
      dest: /tmp
    become: yes                                                                                                                        
    become_user: foobar
```

And consider the fact that the user **foobar** does not exist, so sudo obviously fails.
(This is a minimal reproducer, the real test-case was reported on IRC)

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
A proper error message so the user can fix the real issue.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
Instead we get the bogus error message: **python isn't present on the system.  Unable to compute checksum**

<!--- Paste verbatim command output between quotes below -->
```
[dag@moria ansible.testing]$ ansible-playbook -v test137.yml
Using /home/dag/home-made/ansible.testing/ansible.cfg as config file

PLAY [localhost] **********************************************************************************************************************

TASK [unarchive] **********************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "python isn't present on the system.  Unable to compute checksum"}

PLAY RECAP ****************************************************************************************************************************
localhost                  : ok=0    changed=0    unreachable=0    failed=1
```


Discussion:

Resolving patch:
diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py
--- a/lib/ansible/plugins/action/__init__.py
+++ b/lib/ansible/plugins/action/__init__.py
@@ -443,8 +443,13 @@ def _execute_remote_stat(self, path, all_vars, follow, tmp=None):
         )
         mystat = self._execute_module(module_name='stat', module_args=module_args, task_vars=all_vars, tmp=tmp, delete_remote_tmp=(tmp is None))
 
-        if 'failed' in mystat and mystat['failed']:
-            raise AnsibleError('Failed to get information on remote file (%s): %s' % (path, mystat['msg']))
+        if mystat.get('failed'):
+            msg = mystat.get('module_stderr')
+            if not msg:
+              msg = mystat.get('module_stdout')
+            if not msg:
+              msg = mystat.get('msg')
+            raise AnsibleError('Failed to get information on remote file (%s): %s' % (path, msg))
 
         if not mystat['stat']['exists']:
             # empty might be matched, 1 should never match, also backwards compatible
diff --git a/lib/ansible/plugins/action/template.py b/lib/ansible/plugins/action/template.py
--- a/lib/ansible/plugins/action/template.py
+++ b/lib/ansible/plugins/action/template.py
@@ -25,6 +25,7 @@
 from ansible import constants as C
 from ansible.errors import AnsibleError
 from ansible.module_utils._text import to_bytes, to_native, to_text
+from ansible.module_utils.pycompat24 import get_exception
 from ansible.plugins.action import ActionBase
 from ansible.utils.hashing import checksum_s
 
@@ -43,8 +44,8 @@ def get_checksum(self, dest, all_vars, try_directory=False, source=None, tmp=Non
                 dest = os.path.join(dest, base)
                 dest_stat = self._execute_remote_stat(dest, all_vars=all_vars, follow=False, tmp=tmp)
 
-        except Exception as e:
-            return dict(failed=True, msg=to_bytes(e))
+        except AnsibleError:
+            return dict(failed=True, msg=to_native(get_exception()))
 
         return dest_stat['checksum']
 
@@ -69,9 +70,9 @@ def run(self, tmp=None, task_vars=None):
         else:
             try:
                 source = self._find_needle('templates', source)
-            except AnsibleError as e:
+            except AnsibleError:
                 result['failed'] = True
-                result['msg'] = to_native(e)
+                result['msg'] = to_native(get_exception())
 
         if 'failed' in result:
             return result
diff --git a/lib/ansible/plugins/action/unarchive.py b/lib/ansible/plugins/action/unarchive.py
--- a/lib/ansible/plugins/action/unarchive.py
+++ b/lib/ansible/plugins/action/unarchive.py
@@ -22,6 +22,7 @@
 
 from ansible.errors import AnsibleError
 from ansible.module_utils._text import to_native
+from ansible.module_utils.pycompat24 import get_exception
 from ansible.plugins.action import ActionBase
 from ansible.constants import mk_boolean as boolean
 
@@ -79,19 +80,21 @@ def run(self, tmp=None, task_vars=None):
         if not remote_src:
             try:
                 source = self._loader.get_real_file(self._find_needle('files', source))
-            except AnsibleError as e:
+            except AnsibleError:
                 result['failed'] = True
-                result['msg'] = to_native(e)
+                result['msg'] = to_native(get_exception())
                 self._remove_tmp_path(tmp)
                 return result
 
-        remote_checksum = self._remote_checksum(dest, all_vars=task_vars, follow=True)
-        if remote_checksum == '4':
+        try:
+            remote_stat = self._execute_remote_stat(dest, all_vars=task_vars, follow=True)
+        except AnsibleError:
             result['failed'] = True
-            result['msg'] = "python isn't present on the system.  Unable to compute checksum"
+            result['msg'] = to_native(get_exception())
             self._remove_tmp_path(tmp)
             return result
-        elif remote_checksum != '3':
+
+        if not remote_stat['exists'] or not remote_stat['isdir']:
             result['failed'] = True
             result['msg'] = "dest '%s' must be an existing dir" % dest
             self._remove_tmp_path(tmp)


Test patch: