Repository: ansible/ansible

Issue:
file module always fails to create relative symlinks if force is not yes.
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
module/file
devel/lib/ansible/modules/files/file.py

##### ANSIBLE VERSION
```
ansible --version
ansible 2.2.1.0
  config file = /home/vagrant/ansible/ansible.cfg
  configured module search path = Default w/o overrides
```

##### CONFIGURATION
No changes made.

##### OS / ENVIRONMENT
cat /etc/centos-release
CentOS Linux release 7.3.1611 (Core)

##### SUMMARY
The file module cannot create symlinks: src and destination paths are concatinated and the result is used to check if the src exists.

##### STEPS TO REPRODUCE
Run the task below. The src is a directory that exists. In the dest only ```dias-installer-4.5.0/installer_repository/``` exists.

```yaml
  tasks:
    - name: Create a symlink to the .git folder
      file:
        src: "dias-installer-4.5.0/release_repository.git"
        dest: "dias-installer-4.5.0/installer_repository/.git"
        state: link

```

##### EXPECTED RESULTS
A symlink to be created.

##### ACTUAL RESULTS
The output hints about the issue:
```TASK [Create a symlink to the .git folder] *************************************
fatal: [sseudia23mgt01]: FAILED! => {"changed": false, "failed": true, "msg": "src file does not exist, use \"force=yes\" if you really want to create the link: dias-installer-4.5.0/installer_repository/dias-installer-4.5.0/release_repository.git", "path": "dias-installer-4.5.0/installer_repository/.git", "src": "dias-installer-4.5.0/release_repository.git", "state": "absent"}
```

Note that I am asked if I want to create the link ```dias-installer-4.5.0/installer_repository/dias-installer-4.5.0/release_repository.git``` which is a path created by concatinating the dest and src path, essentially: ```dest/src```. I would not expect such a file to exist and I can only see the path is not created correctly. Perhaps the dest part was intended to be the first part of an absolute path?

When I look into ```https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/files/file.py```
I find the following code:
```
absrc = os.path.join(relpath, src)
        b_absrc = to_bytes(absrc, errors='surrogate_or_strict')
        if not force and not os.path.exists(b_absrc):
            module.fail_json(path=path, src=src, msg='src file does not exist, use "force=yes" if you really want to create the link: %s' % absrc)
```
In the first line, ```relpath```, which originates as the dest or path argument, is joined with the src path. Exactly as I see in the output. In the last part of the code, the code fails if force is not set to yes.


Discussion:
Files identified in the description:
* [lib/ansible/modules/files/file.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/files/file.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
Apologies for taking a long time to look into this.  I have been able to look at it in the past few days, though, and I think this is working as expected.  I'll summarize first and then give a longer explanation afterwards:

I believe for your case, you have a directory tree like:
```
dias-installer-4.5.0
├── installer_repository
├── installer_repository/.git   => release_repository.git
└── release_repository.git
```
And you want to create a link so that the tree looks like this:
```
dias-installer-4.5.0
├── installer_repository
├── installer_repository/.git   => release_repository.git
└── release_repository.git
```

To achieve that you should use a playbook like the following:
```
      file:
        src: "../release_repository.git"
        dest: "dias-installer-4.5.0/installer_repository/.git"
        state: link
```

The reason for this is that ```ansible -m file -a 'src=SRC dest=DEST``` is supposed to work a lot like the UNIX command ```ln -s SRC DEST```.  When you use that with a relative path in SRC, the path is relative to the DEST path, not to the current working directory.  You can test that out by doing somethng like:

```
[pts/40@peru ~]$ cd /home
[pts/40@peru /home]$ ln -s ../home /var/tmp/home
[pts/40@peru /home]$ ls -al /var/tmp/home
lrwxrwxrwx. 1 badger badger 7 Apr 20 12:07 /var/tmp/home -> ../home
```
If you look at the ```ls -al``` output closely, you can see that it is a link to /var/home (since it is relative to the destination) rather than /home (as it would if it was relative to the current working directory).

The documentation for the file module attempts to say this by saying that relative paths are not expanded:
> - src
>         path of the file to link to (applies only to `state=link' and `state=hard'). Will accept absolute, relative and nonexisting paths. Relative paths are not expanded.

but I think that it would be clearer if I mention that it works the same way as ```ln -s``` when presented with relative paths.  I'll go ahead and submit a PR to add that to the documentation.

Resolving patch:
diff --git a/lib/ansible/modules/files/file.py b/lib/ansible/modules/files/file.py
--- a/lib/ansible/modules/files/file.py
+++ b/lib/ansible/modules/files/file.py
@@ -53,8 +53,9 @@
     choices: [ absent, directory, file, hard, link, touch ]
   src:
     description:
-      - path of the file to link to (applies only to C(state=link) and C(state=hard)). Will accept absolute,
-        relative and nonexisting paths. Relative paths are not expanded.
+      - path of the file to link to (applies only to C(state=link) and C(state=hard)). Will accept
+        absolute, relative and nonexisting paths. Relative paths are relative to the file being
+        created (C(path)) which is how the UNIX command C(ln -s SRC DEST) treats relative paths.
   recurse:
     description:
       - recursively set the specified file attributes (applies only to directories)


Test patch:
diff --git a/test/integration/targets/file/tasks/main.yml b/test/integration/targets/file/tasks/main.yml
--- a/test/integration/targets/file/tasks/main.yml
+++ b/test/integration/targets/file/tasks/main.yml
@@ -372,10 +372,36 @@
   file: src=../sub1/file1 dest={{output_dir}}/sub2/link1 state=link
   register: file20_result
 
-- name: verify that the result was marked as changed
+- name: Get stat info for the link
+  stat:
+    path: '{{ output_dir }}/sub2/link1'
+    follow: False
+  register: file20_link_stat
+
+- name: Get stat info for the pointed to file
+  stat:
+    path: '{{ output_dir }}/sub2/link1'
+    follow: True
+  register: file20_links_dest_stat
+
+- name: Get stat info for the file we intend to point to
+  stat:
+    path: '{{ output_dir }}/sub1/file1'
+    follow: False
+  register: file20_dest_stat
+
+- debug: var=file20_dest_stat
+- name: verify that the link was created correctly
   assert:
     that:
+      # file command reports it created something
       - "file20_result.changed == true"
+      # file command created a link
+      - 'file20_link_stat["stat"]["islnk"]'
+      # Link points to the right path
+      - 'file20_link_stat["stat"]["lnk_target"] == "../sub1/file1"'
+      # The link target and the file we intended to link to have the same inode
+      - 'file20_links_dest_stat["stat"]["inode"] == file20_dest_stat["stat"]["inode"]'
 
 - name: create soft link to relative directory
   file: src=sub1 dest={{output_dir}}/sub1-link state=link