diff --git a/docs/docsite/rst/porting_guides/porting_guide_2.7.rst b/docs/docsite/rst/porting_guides/porting_guide_2.7.rst index 4a6c407a4b..ecfb524da8 100644 --- a/docs/docsite/rst/porting_guides/porting_guide_2.7.rst +++ b/docs/docsite/rst/porting_guides/porting_guide_2.7.rst @@ -17,7 +17,14 @@ This document is part of a collection on porting. The complete list of porting g Playbook ======== -No notable changes. +Role Precedence Fix during Role Loading +--------------------------------------- + +Ansible 2.7 makes a small change to variable precedence when loading roles, resolving a bug, ensuring that role loading matches :ref:`variable precedence expectations `. + +Before Ansible 2.7, when loading a role, the variables defined in the role's ``vars/main.yml`` and ``defaults/main.yml`` were not available when parsing the role's ``tasks/main.yml`` file. This prevented the role from utilizing these variables when being parsed. The problem manifested when ``import_tasks`` or ``import_role`` was used with a variable defined in the role's vars or defaults. + +In Ansible 2.7, role ``vars`` and ``defaults`` are now parsed before ``tasks/main.yml``. This can cause a change in behavior if the same variable is defined at the play level and the role level with different values, and leveraged in ``import_tasks`` or ``import_role`` to define the role or file to import. Deprecated ========== diff --git a/lib/ansible/playbook/role/__init__.py b/lib/ansible/playbook/role/__init__.py index 05c515c726..18776a0953 100644 --- a/lib/ansible/playbook/role/__init__.py +++ b/lib/ansible/playbook/role/__init__.py @@ -197,6 +197,19 @@ class Role(Base, Become, Conditional, Taggable): if os.path.isdir(plugin_path): obj.add_directory(plugin_path) + # vars and default vars are regular dictionaries + self._role_vars = self._load_role_yaml('vars', main=self._from_files.get('vars'), allow_dir=True) + if self._role_vars is None: + self._role_vars = dict() + elif not isinstance(self._role_vars, dict): + raise AnsibleParserError("The vars/main.yml file for role '%s' must contain a dictionary of variables" % self._role_name) + + self._default_vars = self._load_role_yaml('defaults', main=self._from_files.get('defaults'), allow_dir=True) + if self._default_vars is None: + self._default_vars = dict() + elif not isinstance(self._default_vars, dict): + raise AnsibleParserError("The defaults/main.yml file for role '%s' must contain a dictionary of variables" % self._role_name) + # load the role's other files, if they exist metadata = self._load_role_yaml('meta') if metadata: @@ -222,19 +235,6 @@ class Role(Base, Become, Conditional, Taggable): raise AnsibleParserError("The handlers/main.yml file for role '%s' must contain a list of tasks" % self._role_name, obj=handler_data, orig_exc=e) - # vars and default vars are regular dictionaries - self._role_vars = self._load_role_yaml('vars', main=self._from_files.get('vars'), allow_dir=True) - if self._role_vars is None: - self._role_vars = dict() - elif not isinstance(self._role_vars, dict): - raise AnsibleParserError("The vars/main.yml file for role '%s' must contain a dictionary of variables" % self._role_name) - - self._default_vars = self._load_role_yaml('defaults', main=self._from_files.get('defaults'), allow_dir=True) - if self._default_vars is None: - self._default_vars = dict() - elif not isinstance(self._default_vars, dict): - raise AnsibleParserError("The defaults/main.yml file for role '%s' must contain a dictionary of variables" % self._role_name) - def _load_role_yaml(self, subdir, main=None, allow_dir=False): file_path = os.path.join(self._role_path, subdir) if self._loader.path_exists(file_path) and self._loader.is_directory(file_path):