Repository: ansible/ansible

Issue:
Role search path precedence change in v2
In 1.9 the path precedence appears to be:
1. playbook_dir/roles/
2. playbook_dir/

In 2.0 the path precedence has changed to what appears to be
1. playbook_dir/
2. playbook_dir/roles/

To validate this, the file tree looks like:

```
.
├── ansible.cfg
├── roles
│   └── test
│       └── tasks
│           └── main.yml
├── test
│   └── tasks
│       └── main.yml
└── test.yml
```

ansible.cfg in this case is completely empty.
#### test.yml

```

---
- hosts: all
  gather_facts: false
  roles:
    - test
```

Each `main.yml` should contain a single debug, each with different message such as:

```
- debug: msg=in_roles_dir
```

```
- debug: msg=in_playbook_dir
```

This causes a completely non-role related directory in the `playbook_dir` to potentially, override an actual role of the same name in the `roles` directory.



Discussion:
The code behind this behaviour is in this file: https://github.com/ansible/ansible/blob/devel/lib/ansible/inventory/__init__.py

Specifically the following code:

``` python
    def _get_hostgroup_vars(self, host=None, group=None, new_pb_basedir=False):
        """
        Loads variables from group_vars/<groupname> and host_vars/<hostname> in directories parallel
        to the inventory base directory or in the same directory as the playbook.  Variables in the playbook
        dir will win over the inventory dir if files are in both.
        """

        results = {}
        scan_pass = 0
        _basedir = self.basedir()

        # look in both the inventory base directory and the playbook base directory
        # unless we do an update for a new playbook base dir
        if not new_pb_basedir:
            basedirs = [_basedir, self._playbook_basedir] # <= here
        else:
            basedirs = [self._playbook_basedir] # <= and here

        for basedir in basedirs:
            # this can happen from particular API usages, particularly if not run
            # from /usr/bin/ansible-playbook
            if basedir in ('', None):
                basedir = './'

```

Lately, I was working on a patch to be able to add additionals directories (you can check it here: https://github.com/ansible/ansible/pull/12628) but I was wondering if it can be interesting to change also this directories.

@Yannig role loading is unrelated to inventory group/host var loading

relevant code seems to be in https://github.com/ansible/ansible/blob/deve/lib/ansible/playbook/role/definition.py#L144 and from my read (as wrong as it looks) `<playdir>/roles` should be read first

but my tests concur, the wrong role gets loaded.

problem is that it matches in line https://github.com/ansible/ansible/blob/deve/lib/ansible/playbook/role/definition.py#140 above


Resolving patch:
diff --git a/lib/ansible/playbook/role/definition.py b/lib/ansible/playbook/role/definition.py
--- a/lib/ansible/playbook/role/definition.py
+++ b/lib/ansible/playbook/role/definition.py
@@ -135,46 +135,44 @@ def _load_role_path(self, role_name):
         append it to the default role path
         '''
 
-        role_path = unfrackpath(role_name)
+        # we always start the search for roles in the base directory of the playbook
+        role_search_paths = [
+            os.path.join(self._loader.get_basedir(), u'roles'),
+            self._loader.get_basedir(),
+        ]
+
+        # also search in the configured roles path
+        if C.DEFAULT_ROLES_PATH:
+            configured_paths = C.DEFAULT_ROLES_PATH.split(os.pathsep)
+            role_search_paths.extend(configured_paths)
+
+        # finally, append the roles basedir, if it was set, so we can
+        # search relative to that directory for dependent roles
+        if self._role_basedir:
+            role_search_paths.append(self._role_basedir)
+
+        # create a templar class to template the dependency names, in
+        # case they contain variables
+        if self._variable_manager is not None:
+            all_vars = self._variable_manager.get_vars(loader=self._loader, play=self._play)
+        else:
+            all_vars = dict()
+
+        templar = Templar(loader=self._loader, variables=all_vars)
+        role_name = templar.template(role_name)
+
+        # now iterate through the possible paths and return the first one we find
+        for path in role_search_paths:
+            path = templar.template(path)
+            role_path = unfrackpath(os.path.join(path, role_name))
+            if self._loader.path_exists(role_path):
+                return (role_name, role_path)
 
+        # if not found elsewhere try to extract path from name
+        role_path = unfrackpath(role_name)
         if self._loader.path_exists(role_path):
             role_name = os.path.basename(role_name)
             return (role_name, role_path)
-        else:
-            # we always start the search for roles in the base directory of the playbook
-            role_search_paths = [
-                os.path.join(self._loader.get_basedir(), u'roles'),
-                u'./roles',
-                self._loader.get_basedir(),
-                u'./'
-            ]
-
-            # also search in the configured roles path
-            if C.DEFAULT_ROLES_PATH:
-                configured_paths = C.DEFAULT_ROLES_PATH.split(os.pathsep)
-                role_search_paths.extend(configured_paths)
-
-            # finally, append the roles basedir, if it was set, so we can
-            # search relative to that directory for dependent roles
-            if self._role_basedir:
-                role_search_paths.append(self._role_basedir)
-
-            # create a templar class to template the dependency names, in
-            # case they contain variables
-            if self._variable_manager is not None:
-                all_vars = self._variable_manager.get_vars(loader=self._loader, play=self._play)
-            else:
-                all_vars = dict()
-
-            templar = Templar(loader=self._loader, variables=all_vars)
-            role_name = templar.template(role_name)
-
-            # now iterate through the possible paths and return the first one we find
-            for path in role_search_paths:
-                path = templar.template(path)
-                role_path = unfrackpath(os.path.join(path, role_name))
-                if self._loader.path_exists(role_path):
-                    return (role_name, role_path)
 
         raise AnsibleError("the role '%s' was not found in %s" % (role_name, ":".join(role_search_paths)), obj=self._ds)
 


Test patch: