Repository: ansible/ansible

Issue:
ansible_env undefined in playbook, documentation example broken
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
playbook

##### ANSIBLE VERSION
2.0.1.0 - 2.2.2.0

##### OS / ENVIRONMENT
Ubuntu 16.04

##### SUMMARY
Example in documentation doesn't work.

In the documentation:

https://github.com/ansible/ansible/blob/devel/docs/docsite/rst/playbooks_environment.rst#working-with-language-specific-version-managers

An example is given to set the PATH when running tasks in a playbook:

```
 environment:
    NVM_DIR: /var/local/nvm
    PATH: /var/local/nvm/versions/node/v4.2.1/bin:{{ ansible_env.PATH }}
```

This doesn't seem to actually work in 2.2.0.0 any longer.

##### STEPS TO REPRODUCE
I've created a simplified version of this example:

```
- hosts: all
  become: false

  vars:
    node_app_dir: /var/local/my_node_app

  environment:
    PATH: "/var/local/nvm/versions/node/v4.2.1/bin:{{ ansible_env.PATH }}"

  tasks:
  - name: test path
    debug:
      msg: "{{ PATH }}"
```

Unfortunately running this playbook gives an error that ansible_env isn't defined.

> ansible-playbook -i "hx_login," test.yml

##### EXPECTED RESULTS
$PATH set to "/var/local/nvm/versions/node/v4.2.1/bin:{{ ansible_env.PATH }}"

##### ACTUAL RESULTS
```
< TASK [setup] >
 --------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

fatal: [hx_login]: FAILED! => {"failed": true, "msg": "the field 'environment' has an invalid value, which appears to include a variable that is undefined. The error was: 'ansible_env' is undefined"}
```

I stepped back through the releases and the last release this functionality works is 2.0.0.2. It breaks at 2.0.1.0.

I truly hope this is a bug and not a depreciated piece of functionality that is accidentally still in the documentation. Thanks.



Discussion:
It seems to me environment broke the playbook. If you commented it out, then it works.
This seems to be a documentation error. I would suggest using lookup('env', 'PATH') instead as it is 
well supported. I will make a pull request to fix the documentation.  <- Nevermind - see bcoca's answer.
the environment is being applied to the 'implicit fact gathering' task, which sets ansible_env .. which is then undefined, just use default filter to avoid this issue:
```
 environment:
    PATH: "/var/local/nvm/versions/node/v4.2.1/bin:{{ (ansible_env|default({})).PATH|default('') }}"
```
Err, I think must have failed to report this clearly enough...

lookup('env', 'PATH') isn't the correct solution, from what I understand that would get the path on the local machine the playbook is being run on, not from the remote machine which is what the documentation is illustrating.

Using the default() filter is very obviously the incorrect solution because it would just mask the bug and doesn't at all do what the documentation is illustrating, getting the remote PATH and appending it to the PATH that is sent when a task is run.

This functionality works until 2.0.0.2 then suddenly breaks. It's described in the official documentation. And is actually very important functionality to be able to append to the default PATH (or any other default variable) on the remote host the play is being executed against. Unless this existing, documented functionality is being dropped, without a deprecation warning, then I'm sorry, this IS actually a bug. I would appreciate if you could take a second look at this issue.

Thanks!

@lairdm Yes, I was wrong. I'm sorry! bcoca has given you the solution though and fixed the documentation.
@lairdm the functionality has not been dropped, the only thing that changed is that the 'fact gathering' task is now affected by environment (it was not getting the PATH before either).

Consider that the task that sets `ansible_env` cannot itself depend on `ansible_env`.
Is this environment thing related to task?
Is this by design?

Test case 1:
```
- hosts: localhost
  become: false

  vars:
    node_app_dir: /var/local/my_node_app

  environment:
    PATH: "/var/local/nvm/versions/node/v4.2.1/bin:{{ (ansible_env|default({})).PATH|default('') }}"

  tasks:
  - name: test path
    debug:
      msg: "{{ PATH }}"
```
Output:
```
TASK [test path] ***********************************************************************************************
task path: /home/ubuntu/ansible/23180.yml:12
fatal: [localhost]: FAILED! => {
    "failed": true,
    "msg": "the field 'args' has an invalid value, which appears to include a variable that is undefined. The error was: 'PATH' is undefined\n\nThe error appears to have been in '/home/ubuntu/ansible/23180.yml': line 12, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n  tasks:\n  - name: test path\n    ^ here\n"
}
```

Test case 2:
```
- hosts: localhost
  become: false

  vars:
    node_app_dir: /var/local/my_node_app

  environment:
    PATH: "/var/local/nvm/versions/node/v4.2.1/bin:{{ (ansible_env|default({})).PATH|default('') }}"

  tasks:
  - name: test path
    debug:
      msg: "{{ ansible_env.PATH }}"
```
Output:
```
TASK [test path] ***********************************************************************************************
task path: /home/ubuntu/ansible/23180.yml:12
ok: [localhost] => {
    "changed": false,
    "msg": "/var/local/nvm/versions/node/v4.2.1/bin:"
}
```
```
* from 2.0.1 onwards, the implicit setup task from gather_facts now correctly inherits everything from play, but this might cause issues for those setting
  `environment` at the play level and depending on `ansible_env` existing. Previouslly this was ignored but now might issue an 'Undefined' error.
```
Ahh, alright I see now that I'm in the office to give it a test. Thanks for the documentation fix. One little quirk I do still see is you can't then use this modified variable locally, ie.

```
  - name: test path
    debug:
      msg: "{{ PATH }}"
```

Will still give an error like:
```
< TASK [test path] >
 ------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

fatal: [hx_login]: FAILED! => {"failed": true, "msg": "the field 'args' has an invalid value, which appears to include a variable that is undefined. The error was: 'PATH' is undefined\n\nThe error appears to have been in '/home/user/rest_ansible/test.yml': line 22, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n  - name: test path\n    ^ here\n"}
```

But indeed the PATH is now set on the remote host when executing tasks. Thanks.

Sorry, I spoke too soon. It no longer errors during the Setup step with the documented change, but it doesn't appear to actually work as advertised.

```
- hosts: localhost
  become: false

  vars:
    node_app_dir: /var/local/my_node_app

  environment:
    PATH: "/var/local/nvm/versions/node/v4.2.1/bin:{{ (ansible_env|default({})).PATH|default('') }}"

  tasks:
  - name: test path
    shell: echo $PATH >path.txt
```

The file created on the remote host contains

```
/var/local/nvm/versions/node/v4.2.1/bin:/var/local/nvm/versions/node/v4.2.1/bin:
```

And not the new path fragment appended to the existing PATH on the remote host. This is using version 2.2.0.0.

Thanks.

Hello,
I have posted a same issue [#22925](https://github.com/ansible/ansible/issues/22925) , [nitzmahone ](https://github.com/nitzmahone) gave me a workaround and he closed the request.
But the way to initialize ansible_env by calling one play just before the real one, for me is not a real elegant solution...
@s-hertel like to know more about the issue.
@snowsky I'm probably not the best one to ask about it. I'm still musing about it.

Resolving patch:
diff --git a/lib/ansible/playbook/block.py b/lib/ansible/playbook/block.py
--- a/lib/ansible/playbook/block.py
+++ b/lib/ansible/playbook/block.py
@@ -280,7 +280,7 @@ def set_loader(self, loader):
                 dep.set_loader(loader)
 
     def _get_attr_environment(self):
-        return self._get_parent_attribute('environment', extend=True)
+        return self._get_parent_attribute('environment', extend=True, prepend=True)
 
     def _get_parent_attribute(self, attr, extend=False, prepend=False):
         '''
diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py
--- a/lib/ansible/playbook/task.py
+++ b/lib/ansible/playbook/task.py
@@ -21,7 +21,7 @@
 
 import os
 
-from ansible.errors import AnsibleError, AnsibleParserError
+from ansible.errors import AnsibleError, AnsibleParserError, AnsibleUndefinedVariable
 from ansible.module_utils.six import iteritems, string_types
 from ansible.module_utils._text import to_native
 from ansible.parsing.mod_args import ModuleArgsParser
@@ -258,25 +258,40 @@ def _post_validate_environment(self, attr, value, templar):
         Override post validation of vars on the play, as we don't want to
         template these too early.
         '''
-        if value is None:
-            return dict()
-
-        elif isinstance(value, list):
-            if len(value) == 1:
-                return templar.template(value[0], convert_bare=True)
-            else:
-                env = []
+        env = {}
+        if value is not None:
+
+            print(value)
+            def _parse_env_kv(k, v):
+                try:
+                    env[k] = templar.template(v, convert_bare=False)
+                except AnsibleUndefinedVariable as e:
+                    if self.action in ('setup', 'gather_facts') and 'ansible_env' in to_native(e):
+                        # ignore as fact gathering sets ansible_env
+                        pass
+
+            if isinstance(value, list):
                 for env_item in value:
-                    if isinstance(env_item, (string_types, AnsibleUnicode)) and env_item in templar._available_variables:
-                        env[env_item] = templar.template(env_item, convert_bare=False)
-        elif isinstance(value, dict):
-            env = dict()
-            for env_item in value:
-                if isinstance(env_item, (string_types, AnsibleUnicode)) and env_item in templar._available_variables:
-                    env[env_item] = templar.template(value[env_item], convert_bare=False)
+                    if isinstance(env_item, dict):
+                        for k in env_item:
+                            _parse_env_kv(k, env_item[k])
+                    else:
+                        isdict = templar.template(env_item, convert_bare=False)
+                        if isinstance(isdict, dict):
+                            env.update(isdict)
+                        else:
+                            display.warning("could not parse environment value, skipping: %s" % value)
+
+            elif isinstance(value, dict):
+                # should not really happen
+                env = dict()
+                for env_item in value:
+                    _parse_env_kv(env_item, value[env_item])
+            else:
+                # at this point it should be a simple string, also should not happen
+                env = templar.template(value, convert_bare=False)
 
-        # at this point it should be a simple string
-        return templar.template(value, convert_bare=True)
+        return env
 
     def _post_validate_changed_when(self, attr, value, templar):
         '''
@@ -410,7 +425,7 @@ def _get_attr_environment(self):
         '''
         Override for the 'tags' getattr fetcher, used from Base.
         '''
-        return self._get_parent_attribute('environment', extend=True)
+        return self._get_parent_attribute('environment', extend=True, prepend=True)
 
     def get_dep_chain(self):
         if self._parent:


Test patch:
diff --git a/test/integration/targets/environment/test_environment.yml b/test/integration/targets/environment/test_environment.yml
--- a/test/integration/targets/environment/test_environment.yml
+++ b/test/integration/targets/environment/test_environment.yml
@@ -2,7 +2,17 @@
   vars:
     - test1:
         key1: val1
+  environment:
+    PATH: '{{ansible_env.PATH + ":/lola"}}'
+    lola: 'ido'
   tasks:
+    - name: ensure special case with ansible_env is skipped but others still work
+      assert:
+        that:
+            - lookup('env', 'PATH') == ansible_env.PATH
+            - "'/lola' not in ansible_env.PATH"
+            - ansible_env.lola == 'ido'
+
     - name: check that envvar does not exist
       shell: echo $key1
       register: test_env