Skip to content

Commit 504d738

Browse files
committed
Add dymanic blocks
1 parent ee68079 commit 504d738

File tree

13 files changed

+5094
-1
lines changed

13 files changed

+5094
-1
lines changed

lib/ansible/executor/task_executor.py

+10
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,16 @@ def _execute(self, variables=None):
583583
include_file = templar.template(include_file)
584584
return dict(include=include_file, include_args=include_args)
585585

586+
# if this task is a TaskDo, we just return now with a success code so the
587+
# main thread can expand the task list for the given host
588+
if self._task.action == 'do':
589+
do_args = self._task.args.copy()
590+
do_block = do_args.get('_raw_params', None)
591+
if not do_block:
592+
return dict(failed=True, msg="Do block was specified without a body.")
593+
594+
return dict(do=do_block, do_args=do_args)
595+
586596
# if this task is a IncludeRole, we just return now with a success code so the main thread can expand the task list for the given host
587597
elif self._task.action == 'include_role':
588598
include_args = self._task.args.copy()

lib/ansible/modules/do.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# Copyright: Ansible Project
5+
# Copyright: Estelle Poulin <[email protected]>
6+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
7+
8+
from __future__ import absolute_import, division, print_function
9+
__metaclass__ = type
10+
11+
12+
DOCUMENTATION = r'''
13+
---
14+
author: Estelle Poulin ([email protected])
15+
module: do
16+
short_description: Dynamically include a task block
17+
description:
18+
- Includes a block with a list of tasks to be executed in the current playbook.
19+
version_added: '2.10'
20+
options:
21+
apply:
22+
description:
23+
- Accepts a hash of task keywords (e.g. C(tags), C(become)) that will be applied to the tasks within the include.
24+
type: str
25+
version_added: '2.7'
26+
free-form:
27+
description:
28+
- |
29+
Accepts a list of tasks specificed in the same manner as C(block).
30+
notes:
31+
- This is a core feature of the Ansible, rather than a module, and cannot be overridden like a module.
32+
seealso:
33+
- module: ansible.builtin.include
34+
- module: ansible.builtin.include_tasks
35+
- module: ansible.builtin.include_role
36+
- ref: playbooks_reuse_includes
37+
description: More information related to including and importing playbooks, roles and tasks.
38+
'''
39+
40+
EXAMPLES = r'''
41+
- hosts: all
42+
tasks:
43+
- debug:
44+
msg: task1
45+
46+
- name: Run a task list within a play.
47+
do:
48+
- debug:
49+
msg: stuff
50+
51+
- debug:
52+
msg: task10
53+
54+
- hosts: all
55+
tasks:
56+
- debug:
57+
msg: task1
58+
59+
- name: Run the task list only if the condition is true.
60+
do:
61+
- debug:
62+
msg: stuff
63+
when: hostvar is defined
64+
65+
- name: Apply tags to tasks within included file
66+
do:
67+
- debug:
68+
msg: stuff
69+
args:
70+
apply:
71+
tags: [install]
72+
tags: [always]
73+
74+
- name: Loop over a block of tasks.
75+
do:
76+
- debug:
77+
var: item
78+
loop: [1, 2, 3]
79+
80+
'''
81+
82+
RETURN = r'''
83+
# This module does not return anything except tasks to execute.
84+
'''

lib/ansible/parsing/mod_args.py

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from ansible.module_utils.six import iteritems, string_types
2424
from ansible.module_utils._text import to_text
2525
from ansible.parsing.splitter import parse_kv, split_args
26+
from ansible.parsing.yaml.objects import AnsibleSequence
2627
from ansible.plugins.loader import module_loader, action_loader
2728
from ansible.template import Templar
2829
from ansible.utils.sentinel import Sentinel
@@ -44,6 +45,7 @@
4445
'include_tasks',
4546
'include_role',
4647
'import_tasks',
48+
'do',
4749
'import_role',
4850
'add_host',
4951
'group_by',
@@ -208,6 +210,8 @@ def _normalize_new_style_args(self, thing, action):
208210
elif thing is None:
209211
# this can happen with modules which take no params, like ping:
210212
args = None
213+
elif isinstance(thing, AnsibleSequence):
214+
args = { u'_raw_params': thing }
211215
else:
212216
raise AnsibleParserError("unexpected parameter type in action: %s" % type(thing), obj=self._task_ds)
213217
return args

lib/ansible/playbook/do_block.py

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# (c) 2012-2014, Michael DeHaan <[email protected]>
2+
# (c) 2020, Estelle Poulin <[email protected]>
3+
#
4+
# This file is part of Ansible
5+
#
6+
# Ansible is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# Ansible is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
18+
19+
# Make coding more python3-ish
20+
from __future__ import (absolute_import, division, print_function)
21+
__metaclass__ = type
22+
23+
import os
24+
25+
from ansible.errors import AnsibleError
26+
from ansible.module_utils._text import to_text
27+
from ansible.playbook.task_include import TaskInclude
28+
from ansible.template import Templar
29+
from ansible.utils.display import Display
30+
31+
display = Display()
32+
33+
34+
class DoBlock:
35+
36+
def __init__(self, block, args, vars, task):
37+
self._block = block
38+
self._args = args
39+
self._vars = vars
40+
self._task = task
41+
self._hosts = []
42+
43+
def add_host(self, host):
44+
if host not in self._hosts:
45+
self._hosts.append(host)
46+
return
47+
raise ValueError()
48+
49+
def __eq__(self, other):
50+
return (other._args == self._args and
51+
other._vars == self._vars and
52+
other._task._uuid == self._task._uuid and
53+
other._task._parent._uuid == self._task._parent._uuid)
54+
55+
def __repr__(self):
56+
return "do_block (args=%s vars=%s): %s" % (self._args, self._vars, self._hosts)
57+
58+
@staticmethod
59+
def process_do_results(results, iterator, loader, variable_manager):
60+
do_blocks = []
61+
task_vars_cache = {}
62+
63+
for res in results:
64+
65+
original_host = res._host
66+
original_task = res._task
67+
68+
if original_task.action in ('do'):
69+
if original_task.loop:
70+
if 'results' not in res._result:
71+
continue
72+
do_results = res._result['results']
73+
else:
74+
do_results = [res._result]
75+
76+
for do_result in do_results:
77+
# if the task result was skipped or failed, continue
78+
if 'skipped' in do_result and do_result['skipped'] or 'failed' in do_result and do_result['failed']:
79+
continue
80+
81+
cache_key = (iterator._play, original_host, original_task)
82+
try:
83+
task_vars = task_vars_cache[cache_key]
84+
except KeyError:
85+
task_vars = task_vars_cache[cache_key] = variable_manager.get_vars(play=iterator._play, host=original_host, task=original_task)
86+
87+
do_args = do_result.get('do_args', dict())
88+
special_vars = {}
89+
loop_var = do_result.get('ansible_loop_var', 'item')
90+
index_var = do_result.get('ansible_index_var')
91+
if loop_var in do_result:
92+
task_vars[loop_var] = special_vars[loop_var] = do_result[loop_var]
93+
if index_var and index_var in do_result:
94+
task_vars[index_var] = special_vars[index_var] = do_result[index_var]
95+
if '_ansible_item_label' in do_result:
96+
task_vars['_ansible_item_label'] = special_vars['_ansible_item_label'] = do_result['_ansible_item_label']
97+
if 'ansible_loop' in do_result:
98+
task_vars['ansible_loop'] = special_vars['ansible_loop'] = do_result['ansible_loop']
99+
if original_task.no_log and '_ansible_no_log' not in do_args:
100+
task_vars['_ansible_no_log'] = special_vars['_ansible_no_log'] = original_task.no_log
101+
102+
# get search path for this task to pass to lookup plugins that may be used in pathing to
103+
# the do block
104+
task_vars['ansible_search_path'] = original_task.get_search_path()
105+
106+
# ensure basedir is always in (dwim already searches here but we need to display it)
107+
if loader.get_basedir() not in task_vars['ansible_search_path']:
108+
task_vars['ansible_search_path'].append(loader.get_basedir())
109+
110+
do_block = original_task
111+
112+
do_blk = DoBlock(do_block, do_args, special_vars, original_task)
113+
114+
idx = 0
115+
orig_do_blk = do_blk
116+
while 1:
117+
try:
118+
pos = do_blocks[idx:].index(orig_do_blk)
119+
# pos is relative to idx since we are slicing
120+
# use idx + pos due to relative indexing
121+
do_blk = do_blocks[idx + pos]
122+
except ValueError:
123+
do_blocks.append(orig_do_blk)
124+
do_blk = orig_do_blk
125+
126+
try:
127+
do_blk.add_host(original_host)
128+
except ValueError:
129+
# The host already exists for this do block, advance forward, this is a new do block
130+
idx += pos + 1
131+
else:
132+
break
133+
134+
return do_blocks

lib/ansible/playbook/task_include.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def get_vars(self):
104104
we need to include the args of the include into the vars as
105105
they are params to the included tasks. But ONLY for 'include'
106106
'''
107-
if self.action != 'include':
107+
if self.action not in ('include', 'do') :
108108
all_vars = super(TaskInclude, self).get_vars()
109109
else:
110110
all_vars = dict()

lib/ansible/plugins/strategy/__init__.py

+75
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,20 @@ def _copy_included_file(self, included_file):
778778

779779
return ti_copy
780780

781+
def _copy_do(self, do_block):
782+
'''
783+
A proven safe and performant way to create a copy of do block.
784+
'''
785+
ti_copy = do_block._task.copy(exclude_parent=True)
786+
ti_copy._parent = do_block._task._parent
787+
788+
temp_vars = ti_copy.vars.copy()
789+
temp_vars.update(do_block._vars)
790+
791+
ti_copy.vars = temp_vars
792+
793+
return ti_copy
794+
781795
def _load_included_file(self, included_file, iterator, is_handler=False):
782796
'''
783797
Loads an included YAML file of tasks, applying the optional set of variables.
@@ -843,6 +857,67 @@ def _load_included_file(self, included_file, iterator, is_handler=False):
843857
display.debug("done processing included file")
844858
return block_list
845859

860+
###
861+
def _load_do_block(self, do_block, iterator, is_handler=False):
862+
'''
863+
Loads a list of tasks from the args of a do block, applying the optional set of variables.
864+
'''
865+
866+
try:
867+
data = do_block._args['_raw_params']
868+
if data is None:
869+
return []
870+
elif not isinstance(data, list):
871+
raise AnsibleError("do blocks must contain a list of tasks")
872+
873+
ti_copy = self._copy_do(do_block)
874+
# pop tags out of the do args, if they were specified there, and assign
875+
# them to the block. If the do already had tags specified, we raise an
876+
# error so that users know not to specify them both ways
877+
tags = do_block._task.vars.pop('tags', [])
878+
if isinstance(tags, string_types):
879+
tags = tags.split(',')
880+
if len(tags) > 0:
881+
if len(do_block._task.tags) > 0:
882+
raise AnsibleParserError("Do blocks should not specify tags in more than one way (both via args and directly on the task). "
883+
"Mixing tag specify styles is prohibited for whole import hierarchy, not only for single import statement",
884+
obj=do_block._task._ds)
885+
display.deprecated("You should not specify tags in the do parameters. All tags should be specified using the task-level option",
886+
version='2.12', collection_name='ansible.builtin')
887+
do_block._task.tags = tags
888+
889+
block_list = load_list_of_blocks(
890+
data,
891+
play=iterator._play,
892+
parent_block=ti_copy,
893+
role=do_block._task._role,
894+
use_handlers=is_handler,
895+
loader=self._loader,
896+
variable_manager=self._variable_manager,
897+
)
898+
899+
# since we skip incrementing the stats when the task result is
900+
# first processed, we do so now for each host in the list
901+
for host in do_block._hosts:
902+
self._tqm._stats.increment('ok', host.name)
903+
904+
except AnsibleError as e:
905+
reason = to_text(e)
906+
907+
# mark all of the hosts including this file as failed, send callbacks,
908+
# and increment the stats for this host
909+
for host in do_block._hosts:
910+
tr = TaskResult(host=host, task=do_block._task, return_data=dict(failed=True, reason=reason))
911+
iterator.mark_host_failed(host)
912+
self._tqm._failed_hosts[host.name] = True
913+
self._tqm._stats.increment('failures', host.name)
914+
self._tqm.send_callback('v2_runner_on_failed', tr)
915+
return []
916+
917+
display.debug("done processing do blocks")
918+
return block_list
919+
920+
###
846921
def run_handlers(self, iterator, play_context):
847922
'''
848923
Runs handlers on those hosts which have been notified.

0 commit comments

Comments
 (0)