Repository: ansible/ansible-modules-core

Issue:
`file` module unexpectedly makes symlink point to `None`
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
file

##### ANSIBLE VERSION
```
ansible 2.2.0.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
```

##### CONFIGURATION
Not changed.

##### OS / ENVIRONMENT
Archlinux

##### SUMMARY
The "file" module makes a target symbolic link point to `None` when the `follow` option is enabled.

##### STEPS TO REPRODUCE

I can reproduce it locally but it originally happened on a remote host.
```
touch private_file
ln -s private_file link
ansible localhost -m file -a 'path=/home/bertrand/tmp/ansi_file_fail/link mode=0600 follow=yes'
```

##### EXPECTED RESULTS
```
> ls -l
total 0
lrwxrwxrwx 1 bertrand bertrand 12 Nov 17 23:53 link -> private_file
-rw------- 1 bertrand bertrand  0 Nov 17 23:37 private_file
```

##### ACTUAL RESULTS

```
> ls -l
total 0
lrwxrwxrwx 1 bertrand bertrand 4 Nov 17 23:56 link -> None
-rw------- 1 bertrand bertrand 0 Nov 17 23:37 private_file
```

The `private_file` permissions are set correctly but the `link` points to `None`, which is unexpected. The commands works fine by addind `state=file` to the arguments.

```
Using /etc/ansible/ansible.cfg as config file
 [WARNING]: Host file not found: /etc/ansible/hosts

 [WARNING]: provided hosts list is empty, only localhost is available

Loading callback plugin minimal of type stdout, v2.0 from /usr/lib/python2.7/site-packages/ansible/plugins/callback/__init__.pyc
Using module file /usr/lib/python2.7/site-packages/ansible/modules/core/files/file.py
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: bertrand
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1479423361.23-271120236039234 `" && echo ansible-tmp-1479423361.23-271120236039234="` echo $HOME/.ansible/tmp/ansible-tmp-1479423361.23-271120236039234 `" ) && sleep 0'
<127.0.0.1> PUT /tmp/tmpXLjK8F TO /home/bertrand/.ansible/tmp/ansible-tmp-1479423361.23-271120236039234/file.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /home/bertrand/.ansible/tmp/ansible-tmp-1479423361.23-271120236039234/ /home/bertrand/.ansible/tmp/ansible-tmp-1479423361.23-271120236039234/file.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python2 /home/bertrand/.ansible/tmp/ansible-tmp-1479423361.23-271120236039234/file.py; rm -rf "/home/bertrand/.ansible/tmp/ansible-tmp-1479423361.23-271120236039234/" > /dev/null 2>&1 && sleep 0'
localhost | SUCCESS => {
    "changed": true, 
    "dest": "/home/bertrand/tmp/ansi_file_fail/link", 
    "diff": {
        "after": {
            "path": "/home/bertrand/tmp/ansi_file_fail/link"
        }, 
        "before": {
            "path": "/home/bertrand/tmp/ansi_file_fail/link"
        }
    }, 
    "invocation": {
        "module_args": {
            "backup": null, 
            "content": null, 
            "delimiter": null, 
            "diff_peek": null, 
            "directory_mode": null, 
            "follow": true, 
            "force": false, 
            "group": null, 
            "mode": "0600", 
            "original_basename": null, 
            "owner": null, 
            "path": "/home/bertrand/tmp/ansi_file_fail/link", 
            "recurse": false, 
            "regexp": null, 
            "remote_src": null, 
            "selevel": null, 
            "serole": null, 
            "setype": null, 
            "seuser": null, 
            "src": null, 
            "state": null, 
            "unsafe_writes": null, 
            "validate": null
        }, 
        "module_name": "file"
    }, 
    "src": "/home/bertrand/tmp/ansi_file_fail/private_file", 
    "state": "absent"
}
```



Discussion:
<!-- @bbc2: Thanks for filing this bug report! -->

@ansible ping, this issue is waiting for your response.
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)

<!--- boilerplate: issue_bug_new --->

Reproduced with devel (2.3) on centos7. Testcase ...

```
- hosts: el7host
  user: root
  gather_facts: False
  tasks:

    - shell: >
        rm -f /tmp/link; 
        rm -rf /tmp/privatedir; 
        mkdir /tmp/privatedir; 
        touch /tmp/privatedir/1; 
        ln -s /tmp/privatedir /tmp/link

    - file: 
        path: /tmp/link 
        mode: 0600 
        follow: yes

    - stat:
        path: /tmp/link
      register: lstat
    - assert:
        that:
            - "lstat.stat.lnk_source != '/tmp/None'"
```

Results for all 2.x versions ...

```
v2.0.0-0.6.rc1 ; 0
v2.0.0-0.7.rc2 ; 0
v2.0.0-0.8.rc3 ; 0
v2.0.0-0.9.rc4 ; 0
v2.0.0.0-1 ; 0
v2.0.0.1-1 ; 0
v2.0.0.2-1 ; 0
v2.0.1.0-0.1.rc1 ; 0
v2.0.1.0-0.2.rc2 ; 0
v2.0.1.0-1 ; 0
v2.0.2.0-0.1.rc1 ; 0
v2.0.2.0-0.2.rc2 ; 0
v2.0.2.0-0.3.rc3 ; 0
v2.0.2.0-0.4.rc4 ; 0
v2.0.2.0-1 ; 0
v2.1.0.0-0.1.rc1 ; 0
v2.1.0.0-0.2.rc2 ; 0
v2.1.0.0-0.3.rc3 ; 0
v2.1.0.0-0.4.rc4 ; 0
v2.1.0.0-1 ; 0
v2.1.1.0-0.1.rc1 ; 0
v2.1.1.0-0.2.rc2 ; 0
v2.1.1.0-0.3.rc3 ; 0
v2.1.1.0-0.4.rc4 ; 0
v2.1.1.0-0.5.rc5 ; 0
v2.1.1.0-1 ; 0
v2.1.2.0-0.1.rc1 ; 0
v2.1.2.0-0.2.rc2 ; 0
v2.1.2.0-0.3.rc3 ; 0
v2.1.2.0-0.4.rc4 ; 0
v2.1.2.0-0.5.rc5 ; 0
v2.1.2.0-1 ; 0
v2.1.3.0-0.1.rc1 ; 0
v2.1.3.0-0.2.rc2 ; 0
v2.1.3.0-0.3.rc3 ; 0
v2.1.3.0-1 ; 0
v2.2.0.0-0.1.rc1 ; 2
v2.2.0.0-0.2.rc2 ; 2
v2.2.0.0-0.3.rc3 ; 2
v2.2.0.0-0.4.rc4 ; 2
v2.2.0.0-1 ; 2
devel-f6437f1 ; 2
```

Bisected to https://github.com/ansible/ansible/commit/2392865d69821a50c0c8038e61747b38b9be6b0b for ansible/ansible
https://github.com/ansible/ansible-modules-core/compare/baa2e930bba851fa4c5b697bf3ac0b3f7513de64...269c06a4c9b5f842e508fbe16d956e6c901896ac#diff-ec997744320cc294d6e1d2a1e1b68938

Prototype patch:

```
diff --git a/files/file.py b/files/file.py
index 25b0085..17c7ae5 100644
--- a/files/file.py
+++ b/files/file.py
@@ -238,6 +238,7 @@ def main():
             if follow and state == 'link':
                 # use the current target of the link as the source
                 src = to_native(os.path.realpath(b_path), errors='strict')
+                b_src = to_native(os.path.realpath(b_path), errors='strict')
             else:
                 module.fail_json(msg='src and dest are required for creating links')
```

@jctanner Looks about right.  Just one change I'd recommend.  Since b_src should always be a byte string, use to_bytes:

```
+                b_src = to_bytes(os.path.realpath(b_path), errors='strict')
```


Resolving patch:
diff --git a/files/file.py b/files/file.py
--- a/files/file.py
+++ b/files/file.py
@@ -238,6 +238,7 @@ def main():
             if follow and state == 'link':
                 # use the current target of the link as the source
                 src = to_native(os.path.realpath(b_path), errors='strict')
+                b_src = to_bytes(os.path.realpath(b_path), errors='strict')
             else:
                 module.fail_json(msg='src and dest are required for creating links')
 


Test patch: