Repository: ansible/ansible

Issue:
Using include within an included role from within a role results in AttributeError
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
 - role + include_role + include

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.4.0 (devel d19b4f2977) last updated 2017/07/26 13:45:18 (GMT -400)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/rteague/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/rteague/dev/clusters/aws-c2/ansible/lib/ansible
  executable location = /home/rteague/dev/clusters/aws-c2/ansible/bin/ansible
  python version = 2.7.13 (default, Jun 26 2017, 10:20:05) [GCC 7.1.1 20170622 (Red Hat 7.1.1-3)]
```

##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.
-->
 - N/A

##### SUMMARY
<!--- Explain the problem briefly -->
When running the playbooks defined below, an AttributeError exception is created when unable to parse the path.
 - playbook calls role
 - role calls include_role
 - included role calls include

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->
```
ansible-playbook -i localhost, playbook.yml -vvvv 
```
<!--- Paste example playbooks or commands between quotes below -->
File organization
```
tree .
.
├── playbook.yml
└── roles
    ├── test
    │   └── tasks
    │       └── main.yml
    └── test-1
        └── tasks
            ├── included_file.yml
            └── main.yml

5 directories, 4 files
```

playbook.yml
```yaml
- name: Test Playbook
  hosts: localhost
  connection: local
  gather_facts: no

  roles:
    - test
```

roles/test/tasks/main.yml
```yaml
---
- include_role:
    name: test-1
  vars:
    some_file: included_file
```

roles/test-1/tasks/main.yml
```yaml
---
- name: Include some task file
  include: "{{ some_file }}.yml"
```

roles/test-1/tasks/included_file.yml
```yaml
---
  - debug:
      msg: "included file"
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
Expect the `debug` task in included_file.yml to run.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```bash
$ ansible-playbook -i localhost, playbook.yml -vvvv                                                                                        
ansible-playbook 2.4.0 (devel d19b4f2977) last updated 2017/07/26 13:45:18 (GMT -400)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/rteague/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/rteague/dev/clusters/aws-c2/ansible/lib/ansible
  executable location = /home/rteague/dev/clusters/aws-c2/ansible/bin/ansible-playbook
  python version = 2.7.13 (default, Jun 26 2017, 10:20:05) [GCC 7.1.1 20170622 (Red Hat 7.1.1-3)]
Using /etc/ansible/ansible.cfg as config file
setting up inventory plugins
Set default localhost to localhost
Parsed localhost, inventory source with host_list plugin
[DEPRECATION WARNING]: The use of 'include' for tasks has been deprecated. Use 'import_tasks' for static inclusions or 'include_tasks' for 
dynamic inclusions.
This feature will be removed in a future release. Deprecation warnings can be disabled by setting 
deprecation_warnings=False in ansible.cfg.
Loading callback plugin default of type stdout, v2.0 from /home/rteague/dev/clusters/aws-c2/ansible/lib/ansible/plugins/callback/__init__.pyc

PLAYBOOK: playbook.yml ***********************************************************************************************************************
1 plays in playbook.yml

PLAY [Test Playbook] *************************************************************************************************************************
META: ran handlers

TASK [test-1 : Include some task file] *******************************************************************************************************
task path: /home/rteague/dev/clusters/aws-c2/reproducer/roles/test-1/tasks/main.yml:3
ERROR! Unexpected Exception, this is probably a bug: 'NoneType' object has no attribute 'rfind'
the full traceback was:

Traceback (most recent call last):
  File "/home/rteague/dev/clusters/aws-c2/ansible/bin/ansible-playbook", line 106, in <module>
    exit_code = cli.run()
  File "/home/rteague/dev/clusters/aws-c2/ansible/lib/ansible/cli/playbook.py", line 130, in run
    results = pbex.run()
  File "/home/rteague/dev/clusters/aws-c2/ansible/lib/ansible/executor/playbook_executor.py", line 153, in run
    result = self._tqm.run(play=play)
  File "/home/rteague/dev/clusters/aws-c2/ansible/lib/ansible/executor/task_queue_manager.py", line 289, in run
    play_return = strategy.run(iterator, play_context)
  File "/home/rteague/dev/clusters/aws-c2/ansible/lib/ansible/plugins/strategy/linear.py", line 333, in run
    variable_manager=self._variable_manager
  File "/home/rteague/dev/clusters/aws-c2/ansible/lib/ansible/playbook/included_file.py", line 106, in process_include_results
    parent_include_dir = os.path.dirname(parent_include._role_path)
  File "/usr/lib64/python2.7/posixpath.py", line 122, in dirname
    i = p.rfind('/') + 1
AttributeError: 'NoneType' object has no attribute 'rfind'

```

Here is output with Ansible 2.3.1
```
ansible 2.3.1.0
  config file = /home/rteague/dev/clusters/aws-c2/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.13 (default, Jun 26 2017, 10:20:05) [GCC 7.1.1 20170622 (Red Hat 7.1.1-3)]
```
```bash
$ ansible-playbook -i localhost, playbook.yml -vvvv                                                                                        
Using /etc/ansible/ansible.cfg as config file
Set default localhost to localhost
Loading callback plugin default of type stdout, v2.0 from /usr/lib/python2.7/site-packages/ansible/plugins/callback/__init__.pyc

PLAYBOOK: playbook.yml ***********************************************************************************************************************
1 plays in playbook.yml

PLAY [Test Playbook] *************************************************************************************************************************
META: ran handlers

TASK [test-1 : Include some task file] *******************************************************************************************************
task path: /home/rteague/dev/clusters/aws-c2/reproducer/roles/test-1/tasks/main.yml:3
included: /home/rteague/dev/clusters/aws-c2/reproducer/roles/test-1/tasks/included_file.yml for localhost

TASK [test-1 : debug] ************************************************************************************************************************
task path: /home/rteague/dev/clusters/aws-c2/reproducer/roles/test-1/tasks/included_file.yml:2
ok: [localhost] => {
    "msg": "included file"
}
META: ran handlers
META: ran handlers

PLAY RECAP ***********************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0   

```
This is a variation of the issue #26525 which was fixed in #26601.
Using include within an included role from within a role results in AttributeError
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
 - role + include_role + include

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.4.0 (devel d19b4f2977) last updated 2017/07/26 13:45:18 (GMT -400)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/rteague/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/rteague/dev/clusters/aws-c2/ansible/lib/ansible
  executable location = /home/rteague/dev/clusters/aws-c2/ansible/bin/ansible
  python version = 2.7.13 (default, Jun 26 2017, 10:20:05) [GCC 7.1.1 20170622 (Red Hat 7.1.1-3)]
```

##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.
-->
 - N/A

##### SUMMARY
<!--- Explain the problem briefly -->
When running the playbooks defined below, an AttributeError exception is created when unable to parse the path.
 - playbook calls role
 - role calls include_role
 - included role calls include

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->
```
ansible-playbook -i localhost, playbook.yml -vvvv 
```
<!--- Paste example playbooks or commands between quotes below -->
File organization
```
tree .
.
├── playbook.yml
└── roles
    ├── test
    │   └── tasks
    │       └── main.yml
    └── test-1
        └── tasks
            ├── included_file.yml
            └── main.yml

5 directories, 4 files
```

playbook.yml
```yaml
- name: Test Playbook
  hosts: localhost
  connection: local
  gather_facts: no

  roles:
    - test
```

roles/test/tasks/main.yml
```yaml
---
- include_role:
    name: test-1
  vars:
    some_file: included_file
```

roles/test-1/tasks/main.yml
```yaml
---
- name: Include some task file
  include: "{{ some_file }}.yml"
```

roles/test-1/tasks/included_file.yml
```yaml
---
  - debug:
      msg: "included file"
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
Expect the `debug` task in included_file.yml to run.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```bash
$ ansible-playbook -i localhost, playbook.yml -vvvv                                                                                        
ansible-playbook 2.4.0 (devel d19b4f2977) last updated 2017/07/26 13:45:18 (GMT -400)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/rteague/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/rteague/dev/clusters/aws-c2/ansible/lib/ansible
  executable location = /home/rteague/dev/clusters/aws-c2/ansible/bin/ansible-playbook
  python version = 2.7.13 (default, Jun 26 2017, 10:20:05) [GCC 7.1.1 20170622 (Red Hat 7.1.1-3)]
Using /etc/ansible/ansible.cfg as config file
setting up inventory plugins
Set default localhost to localhost
Parsed localhost, inventory source with host_list plugin
[DEPRECATION WARNING]: The use of 'include' for tasks has been deprecated. Use 'import_tasks' for static inclusions or 'include_tasks' for 
dynamic inclusions.
This feature will be removed in a future release. Deprecation warnings can be disabled by setting 
deprecation_warnings=False in ansible.cfg.
Loading callback plugin default of type stdout, v2.0 from /home/rteague/dev/clusters/aws-c2/ansible/lib/ansible/plugins/callback/__init__.pyc

PLAYBOOK: playbook.yml ***********************************************************************************************************************
1 plays in playbook.yml

PLAY [Test Playbook] *************************************************************************************************************************
META: ran handlers

TASK [test-1 : Include some task file] *******************************************************************************************************
task path: /home/rteague/dev/clusters/aws-c2/reproducer/roles/test-1/tasks/main.yml:3
ERROR! Unexpected Exception, this is probably a bug: 'NoneType' object has no attribute 'rfind'
the full traceback was:

Traceback (most recent call last):
  File "/home/rteague/dev/clusters/aws-c2/ansible/bin/ansible-playbook", line 106, in <module>
    exit_code = cli.run()
  File "/home/rteague/dev/clusters/aws-c2/ansible/lib/ansible/cli/playbook.py", line 130, in run
    results = pbex.run()
  File "/home/rteague/dev/clusters/aws-c2/ansible/lib/ansible/executor/playbook_executor.py", line 153, in run
    result = self._tqm.run(play=play)
  File "/home/rteague/dev/clusters/aws-c2/ansible/lib/ansible/executor/task_queue_manager.py", line 289, in run
    play_return = strategy.run(iterator, play_context)
  File "/home/rteague/dev/clusters/aws-c2/ansible/lib/ansible/plugins/strategy/linear.py", line 333, in run
    variable_manager=self._variable_manager
  File "/home/rteague/dev/clusters/aws-c2/ansible/lib/ansible/playbook/included_file.py", line 106, in process_include_results
    parent_include_dir = os.path.dirname(parent_include._role_path)
  File "/usr/lib64/python2.7/posixpath.py", line 122, in dirname
    i = p.rfind('/') + 1
AttributeError: 'NoneType' object has no attribute 'rfind'

```

Here is output with Ansible 2.3.1
```
ansible 2.3.1.0
  config file = /home/rteague/dev/clusters/aws-c2/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.13 (default, Jun 26 2017, 10:20:05) [GCC 7.1.1 20170622 (Red Hat 7.1.1-3)]
```
```bash
$ ansible-playbook -i localhost, playbook.yml -vvvv                                                                                        
Using /etc/ansible/ansible.cfg as config file
Set default localhost to localhost
Loading callback plugin default of type stdout, v2.0 from /usr/lib/python2.7/site-packages/ansible/plugins/callback/__init__.pyc

PLAYBOOK: playbook.yml ***********************************************************************************************************************
1 plays in playbook.yml

PLAY [Test Playbook] *************************************************************************************************************************
META: ran handlers

TASK [test-1 : Include some task file] *******************************************************************************************************
task path: /home/rteague/dev/clusters/aws-c2/reproducer/roles/test-1/tasks/main.yml:3
included: /home/rteague/dev/clusters/aws-c2/reproducer/roles/test-1/tasks/included_file.yml for localhost

TASK [test-1 : debug] ************************************************************************************************************************
task path: /home/rteague/dev/clusters/aws-c2/reproducer/roles/test-1/tasks/included_file.yml:2
ok: [localhost] => {
    "msg": "included file"
}
META: ran handlers
META: ran handlers

PLAY RECAP ***********************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0   

```
This is a variation of the issue #26525 which was fixed in #26601.


Discussion:

Resolving patch:
diff --git a/lib/ansible/playbook/included_file.py b/lib/ansible/playbook/included_file.py
--- a/lib/ansible/playbook/included_file.py
+++ b/lib/ansible/playbook/included_file.py
@@ -103,13 +103,13 @@ def get_original_host(host):
                                     parent_include = parent_include._parent
                                     continue
                                 if isinstance(parent_include, IncludeRole):
-                                    parent_include_dir = os.path.dirname(parent_include._role_path)
+                                    parent_include_dir = parent_include._role_path
                                 else:
                                     parent_include_dir = os.path.dirname(templar.template(parent_include.args.get('_raw_params')))
-                                if cumulative_path is None:
-                                    cumulative_path = parent_include_dir
-                                elif not os.path.isabs(cumulative_path):
+                                if cumulative_path is not None and not os.path.isabs(cumulative_path):
                                     cumulative_path = os.path.join(parent_include_dir, cumulative_path)
+                                else:
+                                    cumulative_path = parent_include_dir
                                 include_target = templar.template(include_result['include'])
                                 if original_task._role:
                                     new_basedir = os.path.join(original_task._role._role_path, 'tasks', cumulative_path)
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
@@ -127,6 +127,7 @@ def copy(self, exclude_parent=False, exclude_tasks=False):
         new_me._from_files = self._from_files.copy()
         new_me._parent_role = self._parent_role
         new_me._role_name = self._role_name
+        new_me._role_path = self._role_path
 
         return new_me
 


Test patch: