Repository: ansible/ansible

Issue:
Template and copy modules failing when using su
##### Issue Type:

Bug Report
##### Ansible Version:

ansible 1.7 (devel 981d56ba55) last updated 2014/5/21 12:29:12 (GMT -400)
##### Environment:

Running from RHEL 6.2
Managing RHEL 5.8, 5.10, 6.2
##### Summary:

Template and copy modules fail when using su with the message:

{"msg": "Could not replace file: /tmp/ansible-tmp-1400646411.03-30275482183570/source to /tmp/ans_test/gsdummy.txt: [Errno 13] Permission denied: '/tmp/ansible-tmp-1400646411.03-30275482183570/source'", "failed": true}
##### Steps To Reproduce:

Run playbook using the copy module as an su user.

ansible-playbook demo.yml -imy_hosts.yml -u jdoe --ask-su-pass -vvv

```

---
- name: Copy fail as su
  hosts: su_hosts
  su: yes
  su_user: a_user

  tasks:
    - name: Copy dummy file to /tmp
       copy:
               src=dummy.txt
               dest="/tmp/sufail"
               backup=yes
```
##### Expected Results:

Requested files copied
##### Actual Results:

```
SSH password:
su password:

PLAY [Copy fail as su] ********************************************************

TASK: [Copy dummy file to /tmp] ***********************************************

$ ESTABLISH CONNECTION FOR USER: username on PORT 22 TO hostname
$ EXEC /bin/sh -c 'rc=0; [ -r "/tmp/sufail" ] || rc=2; [ -f "/tmp/sufail" ] || rc=1; [ -d "/tmp/sufail" ] && echo 3 && exit 0; (/usr/bin/md5sum /tmp/sufail 2>/dev/null) || (/sbin/md5sum -q /tmp/sufail 2>/dev/null) || (/usr/bin/digest -a md5 /tmp/sufail 2>/dev/null) || (/sbin/md5 -q /tmp/sufail 2>/dev/null) || (/usr/bin/md5 -n /tmp/sufail 2>/dev/null) || (/bin/md5 -q /tmp/sufail 2>/dev/null) || (/usr/bin/csum -h MD5 /tmp/sufail 2>/dev/null) || (/bin/csum -h MD5 /tmp/sufail 2>/dev/null) || (echo "${rc} /tmp/sufail")'
$ EXEC /bin/sh -c 'rc=0; [ -r "/tmp/sufail/dummy.txt" ] || rc=2; [ -f "/tmp/sufail/dummy.txt" ] || rc=1; [ -d "/tmp/sufail/dummy.txt" ] && echo 3 && exit 0; (/usr/bin/md5sum /tmp/sufail/dummy.txt 2>/dev/null) || (/sbin/md5sum -q /tmp/sufail/dummy.txt 2>/dev/null) || (/usr/bin/digest -a md5 /tmp/sufail/dummy.txt 2>/dev/null) || (/sbin/md5 -q /tmp/sufail/dummy.txt 2>/dev/null) || (/usr/bin/md5 -n /tmp/sufail/dummy.txt 2>/dev/null) || (/bin/md5 -q /tmp/sufail/dummy.txt 2>/dev/null) || (/usr/bin/csum -h MD5 /tmp/sufail/dummy.txt 2>/dev/null) || (/bin/csum -h MD5 /tmp/sufail/dummy.txt 2>/dev/null) || (echo "${rc} /tmp/sufail/dummy.txt")'
$ EXEC /bin/sh -c 'mkdir -p /tmp/ansible-tmp-1401139201.48-207276402368289 && chmod a+rx /tmp/ansible-tmp-1401139201.48-207276402368289 && echo /tmp/ansible-tmp-1401139201.48-207276402368289'
$ PUT /fea-adtools-dev/common/home/a_jira/ansible/ansible/dummy.txt TO /tmp/ansible-tmp-1401139201.48-207276402368289/source
$ PUT /tmp/tmpHbFDfz TO /tmp/ansible-tmp-1401139201.48-207276402368289/copy
$ EXEC /bin/sh -c 'chmod a+r /tmp/ansible-tmp-1401139201.48-207276402368289/copy'
$ EXEC /bin/sh -c 'su a_jira /bin/sh -c '"'"'echo SUDO-SUCCESS-zsezsrmctmkiqbvzajgleqmisisjmduw; LC_CTYPE=C LANG=C /usr/bin/python /tmp/ansible-tmp-1401139201.48-207276402368289/copy; rm -rf /tmp/ansible-tmp-1401139201.48-207276402368289/ >/dev/null 2>&1'"'"''
$ EXEC /bin/sh -c 'rm -rf /tmp/ansible-tmp-1401139201.48-207276402368289/ >/dev/null 2>&1'
fatal: [hostname] => failed to parse:
SUDO-SUCCESS-zsezsrmctmkiqbvzajgleqmisisjmduw
{"msg": "Could not replace file: /tmp/ansible-tmp-1401139201.48-207276402368289/source to /tmp/sufail/dummy.txt: [Errno 13] Permission denied: '/tmp/ansible-tmp-1401139201.48-207276402368289/source'", "failed": true}
Exception OSError: (2, 'No such file or directory', '/tmp/sufail/.ansible_tmpSPdetRdummy.txt') in <bound method _TemporaryFileWrapper.__del__ of <closed file '<fdopen>', mode 'w+b' at 0x7faad6e78f60>> ignored


FATAL: all hosts have already failed -- aborting

PLAY RECAP ********************************************************************
Copy dummy file to /tmp ------------------------------------------------- 1.64s
Total elapsed time: ----------------------------------------------------- 1.64s
           to retry, use: --limit @/home/a_jira/su_copy_fail_demo.retry

hostname : ok=0    changed=0    unreachable=1    failed=0

```



Discussion:
The problem is in the basic.py atomic_move method. It is trying to move the source to dest using shutil.move.  A move wont work for su since the source directory is owned by the remote user.

Line 1037 has a check for sudo(os,getenv("SUDO_USER")) and in the case of sudo does a shutil.copy2 instead of shutil.move.

I was able to get it working by always doing shutil.copy2.

I think the fix needs to be adding a check for su and using shutil.copy2 when su is true.  I tried to do this, but it appears the environment variable for both SUDO_USER and SU_USER are 'None' at this point in the code.

Can you please share the playbook you are using and the output from running with "-vvv" ?

Updated with playbook and output

@gezerk I believe the following patch will address the issue, however I can't quite reproduce the issue you're seeing (tried with both root and a non-root remote user). So if you could test this, I'd appreciate it:

<pre>
diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py
index e06b5cf..bd43ede 100644
--- a/lib/ansible/module_utils/basic.py
+++ b/lib/ansible/module_utils/basic.py
@@ -1020,6 +1020,7 @@ class AnsibleModule(object):
                 context = self.selinux_default_context(dest)
 
         creating = not os.path.exists(dest)
+        switched_user = os.getlogin() != pwd.getpwuid(os.getuid())[0]
 
         try:
             # Optimistically try a rename, solves some corner cases and can avoid useless work, throws exception if not atomic.
@@ -1035,7 +1036,7 @@ class AnsibleModule(object):
                 prefix=".ansible_tmp", dir=dest_dir, suffix=dest_file)
 
             try: # leaves tmp file behind when sudo and  not root
-                if os.getenv("SUDO_USER") and os.getuid() != 0:
+                if switched_user and os.getuid() != 0:
                     # cleanup will happen by 'rm' of tempdir
                     # copy2 will preserve some metadata
                     shutil.copy2(src, tmp_dest.name)
@@ -1058,7 +1059,7 @@ class AnsibleModule(object):
             umask = os.umask(0)
             os.umask(umask)
             os.chmod(dest, 0666 ^ umask)
-            if os.getenv("SUDO_USER"):
+            if switched_user:
                 os.chown(dest, os.getuid(), os.getgid())
 
         if self.selinux_enabled():
</pre>

The `getlogin()` call returns the original owner of the controlling process, essentially what the `logname` command returns. So if that does not equal the username of the current effective UID, then the user has either switched credentials via sudo or su.


Resolving patch:
diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py
--- a/lib/ansible/module_utils/basic.py
+++ b/lib/ansible/module_utils/basic.py
@@ -1020,6 +1020,7 @@ def atomic_move(self, src, dest):
                 context = self.selinux_default_context(dest)
 
         creating = not os.path.exists(dest)
+        switched_user = os.getlogin() != pwd.getpwuid(os.getuid())[0]
 
         try:
             # Optimistically try a rename, solves some corner cases and can avoid useless work, throws exception if not atomic.
@@ -1035,7 +1036,7 @@ def atomic_move(self, src, dest):
                 prefix=".ansible_tmp", dir=dest_dir, suffix=dest_file)
 
             try: # leaves tmp file behind when sudo and  not root
-                if os.getenv("SUDO_USER") and os.getuid() != 0:
+                if switched_user and os.getuid() != 0:
                     # cleanup will happen by 'rm' of tempdir
                     # copy2 will preserve some metadata
                     shutil.copy2(src, tmp_dest.name)
@@ -1058,7 +1059,7 @@ def atomic_move(self, src, dest):
             umask = os.umask(0)
             os.umask(umask)
             os.chmod(dest, 0666 ^ umask)
-            if os.getenv("SUDO_USER"):
+            if switched_user:
                 os.chown(dest, os.getuid(), os.getgid())
 
         if self.selinux_enabled():


Test patch: