Repository: ansible/ansible

Issue:
Breaking change in relative path logic between 1.9.4 and 2
##### Issue Type:

This is a bug report.
##### Ansible Version:

1.9.4 and 2.0.0.2
##### Environment:

Ubuntu 14.04
##### Summary:

Ansible 1.x: Relative paths are resolved relative to the playbook's directory.
Ansible 2.x: Relative paths are resolved relative to current working directory.
##### Steps To Reproduce:

Say you have the following project structure under `/home/user/Projects/`:

```
|-project/
    |-ansible/
        |-playbook.yaml
    |-project/
```

And the following tasks in `playbook.yaml`:

```
- local_action: command chdir=../project pwd
  register: result
  become: False
- debug: var=result.stdout
```
##### Expected Results:

1.9.4 produces:

```
TASK: [deploy | debug var=result.stdout] ************************************** 
ok: [example.com] => {
    "var": {
        "result.stdout": "/home/user/Projects/project/project"
    }
}
```
##### Actual Results:

2.0.0.2 produces:

```
TASK [deploy : debug] **********************************************************
ok: [example.com] => {
    "result.stdout": "/home/user/Projects/project"
}
```



Discussion:
relative paths were 'from execution directory' which in most cases was the home of the login user. But in the case of local connections, it should be cwd. 

Is this an expected backwards-incompatible change in Ansible 2? If so, is it documented?

not expected change, as I would call the behavior you describe, a bug. Leaving this open to confirm

#14341 seems related.

I tend to disagree that this is a bug. To me, Ansible 2.x brought some sanity to the path lookups. You don't need to bury your host_vars and groups_vars in your playbooks directory anymore.

Is there any documentation to the expected behavior of relative paths as sources e.g., under what roots Ansible searches for relative paths, in what precedence/order it searches those, etc.? The documentation of the [copy module](http://docs.ansible.com/ansible/copy_module.html) and [template module](http://docs.ansible.com/ansible/template_module.html) documentation say nothing of the expected behavior for relative paths. The [documentation on roles](http://docs.ansible.com/ansible/playbooks_roles.html#roles) says:

> Any copy, script, template or include tasks (in the role) can reference files in roles/x/{files,templates,tasks}/ (dir depends on task) without having to path them relatively or absolutely).

I find the phrase "without having to path them relatively or absolutely" befuddling given that it has to be relative to _somewhere_ Ansible is looking.

That quote just tries to explain a benefit of structuring your resources in a role in a standard way. If you follow the Ansible convention, you don't need to specify paths for those resources when referencing them in modules (`src=template.j2` will work because `template.j2` is where Ansible knows templates are to be found).

@bcoca Does your answer cover my original bug report? If I pass a relative path to the `chdir` param of the command module, what is/should the path be relative to?

sorry, i 'followed the hijack', I'll delete and move to more appropriate spot.

The 'module execution' is relative to the 'connection login path', which has nothing to do with playbooks or roles. So most commonly with ssh this was the login user's home directory, with local it is the cwd, which did change in 2.0. As 1.x did change the ansible cwd to follow the play execution, but 2.0 does not do this as it converts most things to full paths using the 'current basedir' (based on playbook/task location).

This has created a 'less predictable' path for `local: command`, I'm still not sure this is a bug or if we need to make special considerations for shell/command/raw when using local connections.

So I found this in the code for local connection plugin:
`# FIXME: cwd= needs to be set to the basedir of the playbook`
and since no one else has strong opinions on this being a bug or not ... I'm going to try to fix this.


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
@@ -718,7 +718,14 @@ def _low_level_execute_command(self, cmd, sudoable=True, in_data=None, executabl
                 cmd = executable + ' -c ' + pipes.quote(cmd)
 
         display.debug("_low_level_execute_command(): executing: %s" % (cmd,))
-        rc, stdout, stderr = self._connection.exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+        # Change directory to basedir of task for command execution
+        cwd = os.getcwd()
+        os.chdir(self._loader.get_basedir())
+        try:
+            rc, stdout, stderr = self._connection.exec_command(cmd, in_data=in_data, sudoable=sudoable)
+        finally:
+            os.chdir(cwd)
 
         # stdout and stderr may be either a file-like or a bytes object.
         # Convert either one to a text type
diff --git a/lib/ansible/plugins/connection/local.py b/lib/ansible/plugins/connection/local.py
--- a/lib/ansible/plugins/connection/local.py
+++ b/lib/ansible/plugins/connection/local.py
@@ -69,7 +69,6 @@ def exec_command(self, cmd, in_data=None, sudoable=True):
         executable = C.DEFAULT_EXECUTABLE.split()[0] if C.DEFAULT_EXECUTABLE else None
 
         display.vvv(u"EXEC {0}".format(cmd), host=self._play_context.remote_addr)
-        # FIXME: cwd= needs to be set to the basedir of the playbook
         display.debug("opening command with Popen()")
 
         if isinstance(cmd, (text_type, binary_type)):


Test patch:
diff --git a/test/units/plugins/action/test_action.py b/test/units/plugins/action/test_action.py
--- a/test/units/plugins/action/test_action.py
+++ b/test/units/plugins/action/test_action.py
@@ -607,8 +607,10 @@ def build_module_command(env_string, shebang, cmd, arg_path=None, rm_tmp=None):
         self.assertRaises(AnsibleError, action_base._execute_module)
 
     def test_action_base_sudo_only_if_user_differs(self):
+        fake_loader = MagicMock()
+        fake_loader.get_basedir.return_value = os.getcwd()
         play_context = PlayContext()
-        action_base = DerivedActionBase(None, None, play_context, None, None, None)
+        action_base = DerivedActionBase(None, None, play_context, fake_loader, None, None)
         action_base._connection = MagicMock(exec_command=MagicMock(return_value=(0, '', '')))
         action_base._connection._shell = MagicMock(append_command=MagicMock(return_value=('JOINED CMD')))