Repository: ansible/ansible

Issue:
include_role does not import variables from outer role
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
include_role

##### ANSIBLE VERSION
```
ansible 2.2.0.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
```

##### CONFIGURATION
Irrelevant

##### OS / ENVIRONMENT
Ubuntu 16.04 and OS X 10.11 (same behavior)

##### SUMMARY

Using `include_role` does not define variables from the outer role in the inner role, which makes it very cumbersome to combine roles in larger ones with common configuration.

##### STEPS TO REPRODUCE

test.yml
```yaml
- hosts: localhost
  tasks:
    - include_role: name=a
```

roles/a/defaults/main.yml
```yaml
outer_variable: I_SHOULD_BE_DEFINED
```

roles/a/tasks/main.yml
```yaml
- include_role: name=b
```

roles/b/tasks/main.yml
```yaml
- debug: msg={{ outer_variable }}
```

##### EXPECTED RESULTS
Correct value of `outer_variable` is printed when running `ansible-playbook -vv test.yml`

##### ACTUAL RESULTS
```
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: 'outer_variable' is undefined\n\nThe error appears to have been in '/Users/danielkza/ansible/bug_test/roles/b/tasks/main.yml': line 1, column 3, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n- debug: msg={{ outer_variable }}\n  ^ here\nWe could be wrong, but this one looks like it might be an issue with\nmissing quotes.  Always quote template expression brackets when they\nstart a value. For instance:\n\n    with_items:\n      - {{ foo }}\n\nShould be written as:\n\n    with_items:\n      - \"{{ foo }}\"\n"}
```


Discussion:
IMHO that behaviour is correct.
You can explicitly pass variables like that:

```
- name: Pass variables to role
  include_role:
    name: myrole
  vars:
    rolevar1: 'value from task'
```

@krzysztof-magosa If this behavior is expected it should be documented then, as it is inconsistent with the "traditional" role inclusion method.

test-implicit.yml
```yaml
- hosts: localhost
  roles:
    - name: a
```

```
$ ansible-playbook -c local -i localhost, test-implicit.yml
PLAY [localhost] ***************************************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [b : debug] ***************************************************************
ok: [localhost] => {
    "msg": "I_SHOULD_BE_DEFINED"
}

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0
```

---

Unfortunately, explicitly passing variables (which I am actually already doing in the code that led me to the issue) isn't sufficient: it's quite common for the values of those variables to be composed from variables defined in the outer role. That obviously won't work as they won't be present in the included role. They would need to be eagerly evaluated - which is the workaround I've found, by using `set_fact`.
(formatted version of results in original comment)
``` json
{
   "failed" : true,
   "msg" : "the field 'args' has an invalid value, which appears to include a variable that is undefined. The error was: 'outer_variable' is undefined
The error appears to have been in '/Users/danielkza/ansible/bug_test/roles/b/tasks/main.yml': line 1, column 3, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- debug: msg={{ outer_variable }}
  ^ here
We could be wrong, but this one looks like it might be an issue with
missing quotes.  Always quote template expression brackets when they
start a value. For instance:
    with_items:
      - {{ foo }}
Should be written as:
    with_items:
      - "{{ foo }}"
"
}
```
@alikins Thanks! Just to point it out, I tried quoting (my original reproduction case was quoted) and it doesn't change anything, the variable is actually undefined.
Issue is still present in 2.2.1.0-0.1.rc1, even though some fixes for includes are mentioned.

Resolving patch:
diff --git a/lib/ansible/playbook/role_include.py b/lib/ansible/playbook/role_include.py
--- a/lib/ansible/playbook/role_include.py
+++ b/lib/ansible/playbook/role_include.py
@@ -76,10 +76,16 @@ def get_block_list(self, play=None, variable_manager=None, loader=None):
         actual_role = Role.load(ri, myplay, parent_role=self._parent_role, from_files=self._from_files)
         actual_role._metadata.allow_duplicates = self.allow_duplicates
 
-        # compile role
-        blocks = actual_role.compile(play=myplay)
+        # compile role with parent roles as dependencies to ensure they inherit
+        # variables
+        if not self._parent_role:
+            dep_chain = []
+        else:
+            dep_chain = list(self._parent_role._parents)
+            dep_chain.extend(self._parent_role.get_all_dependencies())
+            dep_chain.append(self._parent_role)
 
-        # set parent to ensure proper inheritance
+        blocks = actual_role.compile(play=myplay, dep_chain=dep_chain)
         for b in blocks:
             b._parent = self
 


Test patch:
diff --git a/test/units/playbook/role/test_include_role.py b/test/units/playbook/role/test_include_role.py
new file mode 100644
--- /dev/null
+++ b/test/units/playbook/role/test_include_role.py
@@ -0,0 +1,229 @@
+# (c) 2016, Daniel Miranda <danielkza2@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.compat.tests import unittest
+from ansible.compat.tests.mock import patch
+
+from ansible.playbook import Play
+from ansible.playbook.task import Task
+from ansible.vars import VariableManager
+
+from units.mock.loader import DictDataLoader
+from units.mock.path import mock_unfrackpath_noop
+
+
+def flatten_tasks(tasks):
+    for task in tasks:
+        if isinstance(task, Task):
+            yield task
+        else:
+            for t in flatten_tasks(task.block):
+                yield t
+
+
+class TestIncludeRole(unittest.TestCase):
+
+    def setUp(self):
+        self.var_manager = VariableManager()
+
+        self.loader = DictDataLoader({
+            '/etc/ansible/roles/l1/tasks/main.yml': """
+                - shell: echo 'hello world from l1'
+                - include_role: name=l2
+            """,
+            '/etc/ansible/roles/l1/tasks/alt.yml': """
+                - shell: echo 'hello world from l1 alt'
+                - include_role: name=l2 tasks_from=alt defaults_from=alt
+            """,
+            '/etc/ansible/roles/l1/defaults/main.yml': """
+                test_variable: l1-main
+                l1_variable: l1-main
+            """,
+            '/etc/ansible/roles/l1/defaults/alt.yml': """
+                test_variable: l1-alt
+                l1_variable: l1-alt
+            """,
+            '/etc/ansible/roles/l2/tasks/main.yml': """
+                - shell: echo 'hello world from l2'
+                - include_role: name=l3
+            """,
+            '/etc/ansible/roles/l2/tasks/alt.yml': """
+                - shell: echo 'hello world from l2 alt'
+                - include_role: name=l3 tasks_from=alt defaults_from=alt
+            """,
+            '/etc/ansible/roles/l2/defaults/main.yml': """
+                test_variable: l2-main
+                l2_variable: l2-main
+            """,
+            '/etc/ansible/roles/l2/defaults/alt.yml': """
+                test_variable: l2-alt
+                l2_variable: l2-alt
+            """,
+            '/etc/ansible/roles/l3/tasks/main.yml': """
+                - shell: echo 'hello world from l3'
+            """,
+            '/etc/ansible/roles/l3/tasks/alt.yml': """
+                - shell: echo 'hello world from l3 alt'
+            """,
+            '/etc/ansible/roles/l3/defaults/main.yml': """
+                test_variable: l3-main
+                l3_variable: l3-main
+            """,
+            '/etc/ansible/roles/l3/defaults/alt.yml': """
+                test_variable: l3-alt
+                l3_variable: l3-alt
+            """
+        })
+
+    def tearDown(self):
+        pass
+
+    def get_tasks_vars(self, play, tasks):
+        for task in flatten_tasks(tasks):
+            role = task._role
+            if not role:
+                continue
+
+            yield (role.get_name(),
+                   self.var_manager.get_vars(self.loader, play=play,
+                                             task=task))
+
+    @patch('ansible.playbook.role.definition.unfrackpath',
+           mock_unfrackpath_noop)
+    def test_simple(self):
+
+        """Test one-level include with default tasks and variables"""
+
+        play = Play.load(dict(
+            name="test play",
+            hosts=['foo'],
+            gather_facts=False,
+            tasks=[
+                {'include_role': 'name=l3'}
+            ]
+        ), loader=self.loader, variable_manager=self.var_manager)
+
+        tasks = play.compile()
+        for role, task_vars in self.get_tasks_vars(play, tasks):
+            self.assertEqual(task_vars.get('l3_variable'), 'l3-main')
+            self.assertEqual(task_vars.get('test_variable'), 'l3-main')
+
+    @patch('ansible.playbook.role.definition.unfrackpath',
+           mock_unfrackpath_noop)
+    def test_simple_alt_files(self):
+
+        """Test one-level include with alternative tasks and variables"""
+
+        play = Play.load(dict(
+            name="test play",
+            hosts=['foo'],
+            gather_facts=False,
+            tasks=[
+                {'include_role': 'name=l3 tasks_from=alt defaults_from=alt'}
+            ]
+        ), loader=self.loader, variable_manager=self.var_manager)
+
+        tasks = play.compile()
+        for role, task_vars in self.get_tasks_vars(play, tasks):
+            self.assertEqual(task_vars.get('l3_variable'), 'l3-alt')
+            self.assertEqual(task_vars.get('test_variable'), 'l3-alt')
+
+    @patch('ansible.playbook.role.definition.unfrackpath',
+           mock_unfrackpath_noop)
+    def test_nested(self):
+
+        """
+        Test nested includes with default tasks and variables.
+
+        Variables from outer roles should be inherited, but overriden in inner
+        roles.
+        """
+
+        play = Play.load(dict(
+            name="test play",
+            hosts=['foo'],
+            gather_facts=False,
+            tasks=[
+                {'include_role': 'name=l1'}
+            ]
+        ), loader=self.loader, variable_manager=self.var_manager)
+
+        tasks = play.compile()
+        for role, task_vars in self.get_tasks_vars(play, tasks):
+            # Outer-most role must not have variables from inner roles yet
+            if role == 'l1':
+                self.assertEqual(task_vars.get('l1_variable'), 'l1-main')
+                self.assertEqual(task_vars.get('l2_variable'), None)
+                self.assertEqual(task_vars.get('l3_variable'), None)
+                self.assertEqual(task_vars.get('test_variable'), 'l1-main')
+            # Middle role must have variables from outer role, but not inner
+            elif role == 'l2':
+                self.assertEqual(task_vars.get('l1_variable'), 'l1-main')
+                self.assertEqual(task_vars.get('l2_variable'), 'l2-main')
+                self.assertEqual(task_vars.get('l3_variable'), None)
+                self.assertEqual(task_vars.get('test_variable'), 'l2-main')
+            # Inner role must have variables from both outer roles
+            elif role == 'l3':
+                self.assertEqual(task_vars.get('l1_variable'), 'l1-main')
+                self.assertEqual(task_vars.get('l2_variable'), 'l2-main')
+                self.assertEqual(task_vars.get('l3_variable'), 'l3-main')
+                self.assertEqual(task_vars.get('test_variable'), 'l3-main')
+
+    @patch('ansible.playbook.role.definition.unfrackpath',
+           mock_unfrackpath_noop)
+    def test_nested_alt_files(self):
+
+        """
+        Test nested includes with alternative tasks and variables.
+
+        Variables from outer roles should be inherited, but overriden in inner
+        roles.
+        """
+
+        play = Play.load(dict(
+            name="test play",
+            hosts=['foo'],
+            gather_facts=False,
+            tasks=[
+                {'include_role': 'name=l1 tasks_from=alt defaults_from=alt'}
+            ]
+        ), loader=self.loader, variable_manager=self.var_manager)
+
+        tasks = play.compile()
+        for role, task_vars in self.get_tasks_vars(play, tasks):
+            # Outer-most role must not have variables from inner roles yet
+            if role == 'l1':
+                self.assertEqual(task_vars.get('l1_variable'), 'l1-alt')
+                self.assertEqual(task_vars.get('l2_variable'), None)
+                self.assertEqual(task_vars.get('l3_variable'), None)
+                self.assertEqual(task_vars.get('test_variable'), 'l1-alt')
+            # Middle role must have variables from outer role, but not inner
+            elif role == 'l2':
+                self.assertEqual(task_vars.get('l1_variable'), 'l1-alt')
+                self.assertEqual(task_vars.get('l2_variable'), 'l2-alt')
+                self.assertEqual(task_vars.get('l3_variable'), None)
+                self.assertEqual(task_vars.get('test_variable'), 'l2-alt')
+            # Inner role must have variables from both outer roles
+            elif role == 'l3':
+                self.assertEqual(task_vars.get('l1_variable'), 'l1-alt')
+                self.assertEqual(task_vars.get('l2_variable'), 'l2-alt')
+                self.assertEqual(task_vars.get('l3_variable'), 'l3-alt')
+                self.assertEqual(task_vars.get('test_variable'), 'l3-alt')