diff --git a/lib/ansible/modules/utilities/logic/include_role.py b/lib/ansible/modules/utilities/logic/include_role.py index 9e02c5c1b7..010f597323 100644 --- a/lib/ansible/modules/utilities/logic/include_role.py +++ b/lib/ansible/modules/utilities/logic/include_role.py @@ -26,6 +26,10 @@ description: - This module is also supported for Windows targets. version_added: "2.2" options: + apply: + description: + - Accepts a hash of task keywords (e.g. C(tags), C(become)) that will be applied to the tasks within the include. + version_added: '2.7' name: description: - The name of the role to be executed. @@ -89,6 +93,15 @@ EXAMPLES = """ include_role: name: myrole when: not idontwanttorun + +- name: Apply tags to tasks within included file + include_role: + name: install + apply: + tags: + - install + tags: + - always """ RETURN = """ diff --git a/lib/ansible/modules/utilities/logic/include_tasks.py b/lib/ansible/modules/utilities/logic/include_tasks.py index 67a8681eb4..b02d1264d0 100644 --- a/lib/ansible/modules/utilities/logic/include_tasks.py +++ b/lib/ansible/modules/utilities/logic/include_tasks.py @@ -23,10 +23,20 @@ description: - Includes a file with a list of tasks to be executed in the current playbook. version_added: "2.4" options: - free-form: + file: description: - The name of the imported file is specified directly without any other option. - Unlike M(import_tasks), most keywords, including loops and conditionals, apply to this statement. + version_added: '2.7' + apply: + description: + - Accepts a hash of task keywords (e.g. C(tags), C(become)) that will be applied to the tasks within the include. + version_added: '2.7' + free-form: + description: + - | + Supplying a file name via free-form C(- include_tasks: file.yml) of a file to be included is the equivalent + of specifying an argument of I(file). notes: - This is a core feature of the Ansible, rather than a module, and cannot be overridden like a module. ''' @@ -51,6 +61,24 @@ EXAMPLES = """ - name: Include task list in play only if the condition is true include_tasks: "{{ hostvar }}.yaml" when: hostvar is defined + +- name: Apply tags to tasks within included file + include_tasks: + file: install.yml + apply: + tags: + - install + tags: + - always + +- name: Apply tags to tasks within included file when using free-form + include_tasks: install.yml + args: + apply: + tags: + - install + tags: + - always """ RETURN = """ diff --git a/lib/ansible/playbook/role_include.py b/lib/ansible/playbook/role_include.py index f129c9a30b..e924422ee9 100644 --- a/lib/ansible/playbook/role_include.py +++ b/lib/ansible/playbook/role_include.py @@ -23,6 +23,7 @@ from os.path import basename from ansible.errors import AnsibleParserError from ansible.playbook.attribute import FieldAttribute +from ansible.playbook.block import Block from ansible.playbook.task_include import TaskInclude from ansible.playbook.role import Role from ansible.playbook.role.include import RoleInclude @@ -45,7 +46,7 @@ class IncludeRole(TaskInclude): BASE = ('name', 'role') # directly assigned FROM_ARGS = ('tasks_from', 'vars_from', 'defaults_from') # used to populate from dict in role - OTHER_ARGS = ('private', 'allow_duplicates') # assigned to matching property + OTHER_ARGS = ('apply', 'private', 'allow_duplicates') # assigned to matching property VALID_ARGS = tuple(frozenset(BASE + FROM_ARGS + OTHER_ARGS)) # all valid args # ================================================================================= @@ -134,6 +135,23 @@ class IncludeRole(TaskInclude): from_key = key.replace('_from', '') ir._from_files[from_key] = basename(ir.args.get(key)) + apply_attrs = ir.args.pop('apply', {}) + if apply_attrs and ir.action != 'include_role': + raise AnsibleParserError('Invalid options for %s: apply' % ir.action, obj=data) + elif apply_attrs: + apply_attrs['block'] = [] + p_block = Block.load( + apply_attrs, + play=block._play, + parent_block=block, + role=role, + task_include=task_include, + use_handlers=block._use_handlers, + variable_manager=variable_manager, + loader=loader, + ) + ir._parent = p_block + # manual list as otherwise the options would set other task parameters we don't want. for option in my_arg_names.intersection(IncludeRole.OTHER_ARGS): setattr(ir, option, ir.args.get(option)) diff --git a/lib/ansible/playbook/task_include.py b/lib/ansible/playbook/task_include.py index f8612a02d3..f4266d1b62 100644 --- a/lib/ansible/playbook/task_include.py +++ b/lib/ansible/playbook/task_include.py @@ -19,7 +19,9 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +from ansible.errors import AnsibleParserError from ansible.playbook.attribute import FieldAttribute +from ansible.playbook.block import Block from ansible.playbook.task import Task try: @@ -38,6 +40,10 @@ class TaskInclude(Task): circumstances related to the `- include: ...` task. """ + BASE = frozenset(('file', '_raw_params')) # directly assigned + OTHER_ARGS = frozenset(('apply',)) # assigned to matching property + VALID_ARGS = BASE.union(OTHER_ARGS) # all valid args + # ================================================================================= # ATTRIBUTES @@ -49,8 +55,38 @@ class TaskInclude(Task): @staticmethod def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None): - t = TaskInclude(block=block, role=role, task_include=task_include) - return t.load_data(data, variable_manager=variable_manager, loader=loader) + ti = TaskInclude(block=block, role=role, task_include=task_include) + task = ti.load_data(data, variable_manager=variable_manager, loader=loader) + + # Validate options + my_arg_names = frozenset(task.args.keys()) + + # validate bad args, otherwise we silently ignore + bad_opts = my_arg_names.difference(TaskInclude.VALID_ARGS) + if bad_opts and task.action in ('include_tasks', 'import_tasks'): + raise AnsibleParserError('Invalid options for %s: %s' % (task.action, ','.join(list(bad_opts))), obj=data) + + if not task.args.get('_raw_params'): + task.args['_raw_params'] = task.args.pop('file') + + apply_attrs = task.args.pop('apply', {}) + if apply_attrs and task.action != 'include_tasks': + raise AnsibleParserError('Invalid options for %s: apply' % task.action, obj=data) + elif apply_attrs: + apply_attrs['block'] = [] + p_block = Block.load( + apply_attrs, + play=block._play, + parent_block=block, + role=role, + task_include=task_include, + use_handlers=block._use_handlers, + variable_manager=variable_manager, + loader=loader, + ) + task._parent = p_block + + return task def copy(self, exclude_parent=False, exclude_tasks=False): new_me = super(TaskInclude, self).copy(exclude_parent=exclude_parent, exclude_tasks=exclude_tasks) diff --git a/test/integration/targets/include_import/apply/import_apply.yml b/test/integration/targets/include_import/apply/import_apply.yml new file mode 100644 index 0000000000..27a40861c1 --- /dev/null +++ b/test/integration/targets/include_import/apply/import_apply.yml @@ -0,0 +1,31 @@ +--- +- hosts: testhost + gather_facts: false + tasks: + - import_tasks: + file: import_tasks.yml + apply: + tags: + - foo + tags: + - always + + - assert: + that: + - include_tasks_result is defined + tags: + - always + + - import_role: + name: import_role + apply: + tags: + - foo + tags: + - always + + - assert: + that: + - include_role_result is defined + tags: + - always diff --git a/test/integration/targets/include_import/apply/include_apply.yml b/test/integration/targets/include_import/apply/include_apply.yml new file mode 100644 index 0000000000..8f71530268 --- /dev/null +++ b/test/integration/targets/include_import/apply/include_apply.yml @@ -0,0 +1,31 @@ +--- +- hosts: testhost + gather_facts: false + tasks: + - include_tasks: + file: include_tasks.yml + apply: + tags: + - foo + tags: + - always + + - assert: + that: + - include_tasks_result is defined + tags: + - always + + - include_role: + name: include_role + apply: + tags: + - foo + tags: + - always + + - assert: + that: + - include_role_result is defined + tags: + - always diff --git a/test/integration/targets/include_import/apply/include_tasks.yml b/test/integration/targets/include_import/apply/include_tasks.yml new file mode 100644 index 0000000000..be511d1ea4 --- /dev/null +++ b/test/integration/targets/include_import/apply/include_tasks.yml @@ -0,0 +1,2 @@ +- set_fact: + include_tasks_result: true diff --git a/test/integration/targets/include_import/apply/roles/include_role/tasks/main.yml b/test/integration/targets/include_import/apply/roles/include_role/tasks/main.yml new file mode 100644 index 0000000000..7f86b26406 --- /dev/null +++ b/test/integration/targets/include_import/apply/roles/include_role/tasks/main.yml @@ -0,0 +1,2 @@ +- set_fact: + include_role_result: true diff --git a/test/integration/targets/include_import/runme.sh b/test/integration/targets/include_import/runme.sh index d5d00bb9f1..b8a8a4ce9b 100755 --- a/test/integration/targets/include_import/runme.sh +++ b/test/integration/targets/include_import/runme.sh @@ -67,3 +67,13 @@ ANSIBLE_STRATEGY='free' ansible-playbook undefined_var/playbook.yml -i ../../in # Include path inheritance using host var for include file path ANSIBLE_STRATEGY='linear' ansible-playbook include_path_inheritance/playbook.yml -i ../../inventory "$@" ANSIBLE_STRATEGY='free' ansible-playbook include_path_inheritance/playbook.yml -i ../../inventory "$@" + +# include_ + apply (explicit inheritance) +ANSIBLE_STRATEGY='linear' ansible-playbook apply/include_apply.yml -i ../../inventory "$@" --tags foo +set +e +OUT=$(ANSIBLE_STRATEGY='linear' ansible-playbook apply/import_apply.yml -i ../../inventory "$@" --tags foo 2>&1 | grep 'ERROR! Invalid options for import_tasks: apply') +set -e +if [[ -z "$OUT" ]]; then + echo "apply on import_tasks did not cause error" + exit 1 +fi