Repository: ansible/ansible

Issue:
Ansible will create a literal $HOME directory if the variable is not set
##### ISSUE TYPE

Bug Report
##### ANSIBLE VERSION

ansible 2.1.0.0
##### CONFIGURATION

Nothing relevant
##### OS / ENVIRONMENT

Not platform specific
##### SUMMARY

When the $HOME variable is not set, Ansible creates a literal $HOME directory in the current working directory:

```
(venv) ┬─[dmsimard@hostname:~/dev/ara/$HOME/.ansible]─[10:53:57 AM]
╰─> (static-generation) $ tree
.
└── tmp
    ├── ansible-local-tmp2Gdd9B
    ├── ansible-local-tmp3DDjOt
    ├── ansible-local-tmp3zLvr4
    ├── ansible-local-tmp3ZRISO
    ...
```

![Alt text](http://i.imgur.com/MKBxpc1.png)
##### STEPS TO REPRODUCE

Inventory:

```
localhost  ansible_connection=local
```

Unset $HOME, run Ansible.
I came across this problem when running Ansible from tox without "HOME" being specified in "passenv" so the $HOME variable wasn't passed to the tox environment.
##### EXPECTED RESULTS

I don't expect Ansible to create a literal $HOME directory in the current working directory. At worst, create a ".ansible" folder if $HOME is not set.
##### ACTUAL RESULTS

A $HOME directory is created in the current working directory.



Discussion:
It looks to me like we should probably rely on '~', rather than $HOME, which is properly expanded even if $HOME is not set:

```
[dmsimard@hostname ara]$ env |grep HOME
HOME=/home/dmsimard
[dmsimard@hostname ara]$ python
Python 2.7.11 (default, Mar 31 2016, 20:46:51) 
[GCC 5.3.1 20151207 (Red Hat 5.3.1-2)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> print(os.path.expanduser('~/.ansible'))
/home/dmsimard/.ansible
>>> 
[dmsimard@hostname ara]$ unset HOME
[dmsimard@hostname ara]$ env |grep HOME
[dmsimard@hostname ara]$ python
Python 2.7.11 (default, Mar 31 2016, 20:46:51) 
[GCC 5.3.1 20151207 (Red Hat 5.3.1-2)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> print(os.path.expanduser('~/.ansible'))
/home/dmsimard/.ansible
>>>
```

This would be in constants: https://github.com/ansible/ansible/blob/bf0da4aa3c0417551c9163f13ed05dc5d70322ab/lib/ansible/constants.py#L144-L145

A simple reproducer:

```
env -i /usr/bin/ansible all -m ping
```

This will create a `$HOME` directory in your current directory.

Also, note that this is a problem with more than just `$HOME`; this is a fundamental problem with environment variable expansion via `os.path.expandvars`, whic says:

> Unknown variables are left unchanged.

That means you have no way to tell if there were undefined variables or not, so there's no easy way to detect this problem unless you replace `os.path.expandvars` with something else.

With the fix proposed in https://github.com/ansible/ansible/pull/16034, the behavior is now:

```
$ env -i PYTHONPATH=$PYTHONPATH `which ansible` all -m ping
Traceback (most recent call last):
  File "/home/lars/src/ansible/bin/ansible", line 44, in <module>
    import ansible.constants as C
  File "/home/lars/src/ansible/lib/ansible/constants.py", line 167, in <module>
    DEFAULT_LOCAL_TMP         = get_config(p, DEFAULTS, 'local_tmp',        'ANSIBLE_LOCAL_TEMP',      '$HOME/.ansible/tmp', istmppath=True)
  File "/home/lars/src/ansible/lib/ansible/constants.py", line 92, in get_config
    value = shell_expand(value)
  File "/home/lars/src/ansible/lib/ansible/constants.py", line 69, in shell_expand
    raise AnsibleOptionsError("Undefined environment variable: {0}".format(err))
ansible.errors.AnsibleOptionsError: Undefined environment variable: 'HOME'
```

I'm not sure this is a bug or just expected behavior. 

@bcoca In the worst case scenario, Ansible should create ".ansible" in the cwd, not create a directory literally called "$HOME" and .ansible in it.

@bcoca  I think that treating a variable literally if it doesn't exist is clearly a bug.  It results in a value that is clearly incorrect.  Most languages will throw an error for undefined variables, and the shell will at least replace it with an empty string (which would cause a different set of problems in this case).

its not an ansible variable, you are defining a shell variable and we pass it to the shell, the shell does not complain by default. Not sure why its not being 'emptied'.

For the record, this affects `ansible-pull` as well, creating `$HOME/.ansible/tmp` in pull's working directory and causing multiple `ansible-pull` calls to fail because git's working directory isn't clean. Our work-around is to nuke pull's working directory between runs, but it'd be nice for a proper fix :)

@bcoca:

> its not an ansible variable, you are defining a shell variable and we pass it to the shell

We are not passing the variable to the shell.  We are passing a variable to `os.path.expandvars`.  This is entirely an Ansible bug (because it breaks exactly the way the standard library documentation says it will break).

@chris-west-travelex I'm curious if the fix I proposed in https://github.com/ansible/ansible/pull/16034 looks sane to you?

@larsks: that looks like it'll kill `ansible-pull` altogether if $HOME isn't set. i bet that'd cause problems for people who put it in cron (or cloud-init, like we do). Perhaps it's more that the proposed fix in #16034 is fine, but `ansible-pull` needs to not have a hard dependency on $HOME, as per @dmsimard's comment above.

Note that this problem also exists in ssh.py or path.py and probably a few other places where `expandvars()` is used.

You could use

```
from string import Template
Template(path).substitute(os.environ))
```

instead of the proposed code in order to generate a KeyError exception on an empty environment variable.

Why does this code not respect `local_tmp`, by the way?

If you can, see link, i think i have similar issue with other module where uses os.environ

http://stackoverflow.com/questions/36043393/set-environment-in-ansible-doesnt-seem-to-work/37950548#37950548

FWIW, I'm +1'ing that this is a bug.

+1

It is quite dangerous bug considering one may fall into this trap:

```
rm -rf $HOME 
```

... yup, I did that... #fail

On Mon, Oct 31, 2016 at 11:47 AM Jaroslav Henner notifications@github.com
wrote:

> It is quite dangerous bug considering one may fall into this trap:
> 
> rm -rf $HOME
> 
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> https://github.com/ansible/ansible/issues/16032#issuecomment-257348656,
> or mute the thread
> https://github.com/notifications/unsubscribe-auth/AGD7chaTXuTvTAO7Zj8hwqf9Os90PBvDks5q5huwgaJpZM4IopoL
> .


Resolving patch:
diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py
--- a/lib/ansible/constants.py
+++ b/lib/ansible/constants.py
@@ -174,8 +174,8 @@ def load_config_file():
 DEFAULT_HOST_LIST         = get_config(p, DEFAULTS,'inventory', 'ANSIBLE_INVENTORY', DEPRECATED_HOST_LIST, value_type='path')
 DEFAULT_MODULE_PATH       = get_config(p, DEFAULTS, 'library',          'ANSIBLE_LIBRARY',          None, value_type='pathlist')
 DEFAULT_ROLES_PATH        = get_config(p, DEFAULTS, 'roles_path',       'ANSIBLE_ROLES_PATH',       '/etc/ansible/roles', value_type='pathlist', expand_relative_paths=True)
-DEFAULT_REMOTE_TMP        = get_config(p, DEFAULTS, 'remote_tmp',       'ANSIBLE_REMOTE_TEMP',      '$HOME/.ansible/tmp')
-DEFAULT_LOCAL_TMP         = get_config(p, DEFAULTS, 'local_tmp',        'ANSIBLE_LOCAL_TEMP',      '$HOME/.ansible/tmp', value_type='tmppath')
+DEFAULT_REMOTE_TMP        = get_config(p, DEFAULTS, 'remote_tmp',       'ANSIBLE_REMOTE_TEMP',      '~/.ansible/tmp')
+DEFAULT_LOCAL_TMP         = get_config(p, DEFAULTS, 'local_tmp',        'ANSIBLE_LOCAL_TEMP',      '~/.ansible/tmp', value_type='tmppath')
 DEFAULT_MODULE_NAME       = get_config(p, DEFAULTS, 'module_name',      None,                       'command')
 DEFAULT_FORKS             = get_config(p, DEFAULTS, 'forks',            'ANSIBLE_FORKS',            5, value_type='integer')
 DEFAULT_MODULE_ARGS       = get_config(p, DEFAULTS, 'module_args',      'ANSIBLE_MODULE_ARGS',      '')
@@ -300,7 +300,7 @@ def load_config_file():
 # CONNECTION RELATED
 ANSIBLE_SSH_ARGS               = get_config(p, 'ssh_connection', 'ssh_args', 'ANSIBLE_SSH_ARGS', '-C -o ControlMaster=auto -o ControlPersist=60s')
 ANSIBLE_SSH_CONTROL_PATH       = get_config(p, 'ssh_connection', 'control_path', 'ANSIBLE_SSH_CONTROL_PATH', u"%(directory)s/ansible-ssh-%%h-%%p-%%r")
-ANSIBLE_SSH_CONTROL_PATH_DIR   = get_config(p, 'ssh_connection', 'control_path_dir', 'ANSIBLE_SSH_CONTROL_PATH_DIR', u'$HOME/.ansible/cp')
+ANSIBLE_SSH_CONTROL_PATH_DIR   = get_config(p, 'ssh_connection', 'control_path_dir', 'ANSIBLE_SSH_CONTROL_PATH_DIR', u'~/.ansible/cp')
 ANSIBLE_SSH_PIPELINING         = get_config(p, 'ssh_connection', 'pipelining', 'ANSIBLE_SSH_PIPELINING', False, value_type='boolean')
 ANSIBLE_SSH_RETRIES            = get_config(p, 'ssh_connection', 'retries', 'ANSIBLE_SSH_RETRIES', 0, value_type='integer')
 ANSIBLE_SSH_EXECUTABLE         = get_config(p, 'ssh_connection', 'ssh_executable', 'ANSIBLE_SSH_EXECUTABLE', 'ssh')


Test patch: