diff --git a/lib/ansible/_internal/_task.py b/lib/ansible/_internal/_task.py index 47bbb4f8a58..d19cc283b3e 100644 --- a/lib/ansible/_internal/_task.py +++ b/lib/ansible/_internal/_task.py @@ -202,8 +202,7 @@ class TaskContext(AmbientContextBase): original_task = self.task original_play_context = te._play_context - loop_item_task = self.task.copy(exclude_parent=True, exclude_tasks=True) - loop_item_task._parent = self.task._parent + loop_item_task = self.task.copy() loop_item_play_context = te._play_context.copy() self._task = loop_item_task diff --git a/lib/ansible/executor/play_iterator.py b/lib/ansible/executor/play_iterator.py index 78d95719eb0..cc5402db40a 100644 --- a/lib/ansible/executor/play_iterator.py +++ b/lib/ansible/executor/play_iterator.py @@ -603,23 +603,36 @@ class PlayIterator: if state.tasks_child_state: state.tasks_child_state = self._insert_tasks_into_state(state.tasks_child_state, task_list) else: - target_block = state._blocks[state.cur_block].copy(exclude_tasks=True) - target_block.block[state.cur_regular_task:state.cur_regular_task] = task_list - state._blocks[state.cur_block] = target_block + cur_block = state._blocks[state.cur_block] + if cur_block._copied: + cur_block.block[state.cur_regular_task:state.cur_regular_task] = task_list + else: + target_block = cur_block.copy() + target_block.block[state.cur_regular_task:state.cur_regular_task] = task_list + state._blocks[state.cur_block] = target_block elif state.run_state == IteratingStates.RESCUE: if state.rescue_child_state: state.rescue_child_state = self._insert_tasks_into_state(state.rescue_child_state, task_list) else: - target_block = state._blocks[state.cur_block].copy(exclude_tasks=True) - target_block.rescue[state.cur_rescue_task:state.cur_rescue_task] = task_list - state._blocks[state.cur_block] = target_block + cur_block = state._blocks[state.cur_block] + if cur_block._copied: + cur_block.rescue[state.cur_rescue_task:state.cur_rescue_task] = task_list + else: + target_block = cur_block.copy() + target_block.rescue[state.cur_rescue_task:state.cur_rescue_task] = task_list + state._blocks[state.cur_block] = target_block elif state.run_state == IteratingStates.ALWAYS: if state.always_child_state: state.always_child_state = self._insert_tasks_into_state(state.always_child_state, task_list) else: - target_block = state._blocks[state.cur_block].copy(exclude_tasks=True) - target_block.always[state.cur_always_task:state.cur_always_task] = task_list - state._blocks[state.cur_block] = target_block + cur_block = state._blocks[state.cur_block] + if cur_block._copied: + cur_block.rescue[state.cur_rescue_task:state.cur_rescue_task] = task_list + cur_block.always[state.cur_always_task:state.cur_always_task] = task_list + else: + target_block = cur_block.copy() + target_block.always[state.cur_always_task:state.cur_always_task] = task_list + state._blocks[state.cur_block] = target_block elif state.run_state == IteratingStates.HANDLERS: state.handlers[state.cur_handlers_task:state.cur_handlers_task] = [h for b in task_list for h in b.block] diff --git a/lib/ansible/playbook/base.py b/lib/ansible/playbook/base.py index 0cacb77cd93..cc6354d3f3a 100644 --- a/lib/ansible/playbook/base.py +++ b/lib/ansible/playbook/base.py @@ -9,9 +9,9 @@ import itertools import operator import os +import copy import typing as t -from copy import copy as shallowcopy from functools import cache from ansible import constants as C @@ -423,29 +423,12 @@ class FieldAttributeBase: self._squashed = True def copy(self): - """ - Create a copy of this object and return it. - """ - - try: - new_me = self.__class__() - except RecursionError as ex: - raise AnsibleError("Exceeded maximum object depth. This may have been caused by excessive role recursion.") from ex - + new_me = copy.copy(self) + nd = new_me.__dict__ for name in self.fattributes: - setattr(new_me, name, shallowcopy(getattr(self, f'_{name}', Sentinel))) - - new_me._loader = self._loader - new_me._variable_manager = self._variable_manager - new_me._origin = self._origin - new_me._validated = self._validated - new_me._finalized = self._finalized - new_me._uuid = self._uuid - - # if the ds value was set on the object, copy it to the new copy too - if hasattr(self, '_ds'): - new_me._ds = self._ds - + n = f'_{name}' + if (v := nd.get(n)) is not None and isinstance(v, (list, dict)): + nd[n] = v.copy() return new_me def get_validated_value(self, name, attribute, value, templar): diff --git a/lib/ansible/playbook/block.py b/lib/ansible/playbook/block.py index 8503648f66f..fde878f89d3 100644 --- a/lib/ansible/playbook/block.py +++ b/lib/ansible/playbook/block.py @@ -17,6 +17,8 @@ from __future__ import annotations +import itertools + from ansible.errors import AnsibleParserError from ansible.module_utils.common.sentinel import Sentinel from ansible.playbook.attribute import NonInheritableFieldAttribute @@ -40,13 +42,13 @@ class Block(Base, Conditional, CollectionSearch, Taggable, Notifiable, Delegatab # similar to the 'else' clause for exceptions # otherwise = FieldAttribute(isa='list') - def __init__(self, play=None, parent_block=None, role=None, task_include=None, use_handlers=False, implicit=False): + def __init__(self, play=None, parent_block=None, role=None, task_include=None, use_handlers=False): self._play = play self._role = role self._parent = None self._dep_chain = None self._use_handlers = use_handlers - self._implicit = implicit + self._copied = False if task_include: self._parent = task_include @@ -83,8 +85,7 @@ class Block(Base, Conditional, CollectionSearch, Taggable, Notifiable, Delegatab @staticmethod def load(data, play=None, parent_block=None, role=None, task_include=None, use_handlers=False, variable_manager=None, loader=None): - implicit = not Block.is_block(data) - b = Block(play=play, parent_block=parent_block, role=role, task_include=task_include, use_handlers=use_handlers, implicit=implicit) + b = Block(play=play, parent_block=parent_block, role=role, task_include=task_include, use_handlers=use_handlers) return b.load_data(data, variable_manager=variable_manager, loader=loader) @staticmethod @@ -150,49 +151,32 @@ class Block(Base, Conditional, CollectionSearch, Taggable, Notifiable, Delegatab else: return self._dep_chain[:] - def copy(self, exclude_parent=False, exclude_tasks=False): - def _dupe_task_list(task_list, new_block): + def copy(self): + def _reparent_tasks(task_list, new_block): new_task_list = [] for task in task_list: - new_task = task.copy(exclude_parent=True, exclude_tasks=exclude_tasks) - if task._parent: - new_task._parent = task._parent.copy(exclude_tasks=True) - if task._parent == new_block: - # If task._parent is the same as new_block, just replace it - new_task._parent = new_block - else: - # task may not be a direct child of new_block, search for the correct place to insert new_block - cur_obj = new_task._parent - while cur_obj._parent and cur_obj._parent != new_block: - cur_obj = cur_obj._parent - - cur_obj._parent = new_block - else: + new_task = task.copy() + if task._parent == new_block: + # If task._parent is the same as new_block, just replace it new_task._parent = new_block + else: + # parent is include/import, skip one level + new_task._parent._parent = new_block new_task_list.append(new_task) return new_task_list - new_me = super(Block, self).copy() - new_me._play = self._play - new_me._use_handlers = self._use_handlers + new_me = super().copy() if self._dep_chain is not None: new_me._dep_chain = self._dep_chain[:] - new_me._parent = None - if self._parent and not exclude_parent: - new_me._parent = self._parent.copy(exclude_tasks=True) + # re-parent tasks within the block to point at the new one via _parent + new_me.block = _reparent_tasks(self.block, new_me) + new_me.rescue = _reparent_tasks(self.rescue, new_me) + new_me.always = _reparent_tasks(self.always, new_me) - if not exclude_tasks: - new_me.block = _dupe_task_list(self.block or [], new_me) - new_me.rescue = _dupe_task_list(self.rescue or [], new_me) - new_me.always = _dupe_task_list(self.always or [], new_me) + new_me._copied = True - new_me._role = None - if self._role: - new_me._role = self._role - - new_me.validate() return new_me def set_loader(self, loader): @@ -289,40 +273,26 @@ class Block(Base, Conditional, CollectionSearch, Taggable, Notifiable, Delegatab tmp_list = [] for task in target: if isinstance(task, Block): - filtered_block = evaluate_block(task) + filtered_block = task.filter_tagged_tasks(all_vars) if filtered_block.has_tasks(): tmp_list.append(filtered_block) elif task.evaluate_tags(self._play.only_tags, self._play.skip_tags, all_vars=all_vars): tmp_list.append(task) return tmp_list - def evaluate_block(block): - new_block = block.copy(exclude_parent=True, exclude_tasks=True) - new_block._parent = block._parent - new_block.block = evaluate_and_append_task(block.block) - new_block.rescue = evaluate_and_append_task(block.rescue) - new_block.always = evaluate_and_append_task(block.always) - return new_block - - return evaluate_block(self) + self.block = evaluate_and_append_task(self.block) + self.rescue = evaluate_and_append_task(self.rescue) + self.always = evaluate_and_append_task(self.always) + return self # FIXME def get_tasks(self): - def evaluate_and_append_task(target): - tmp_list = [] - for task in target: - if isinstance(task, Block): - tmp_list.extend(evaluate_block(task)) - else: - tmp_list.append(task) - return tmp_list - - def evaluate_block(block): - rv = evaluate_and_append_task(block.block) - rv.extend(evaluate_and_append_task(block.rescue)) - rv.extend(evaluate_and_append_task(block.always)) - return rv - - return evaluate_block(self) + task_list = [] + for task in itertools.chain(self.block, self.rescue, self.always): + if isinstance(task, Block): + task_list.extend(task.get_tasks()) + else: + task_list.append(task) + return task_list def has_tasks(self): return len(self.block) > 0 or len(self.rescue) > 0 or len(self.always) > 0 diff --git a/lib/ansible/playbook/helpers.py b/lib/ansible/playbook/helpers.py index e3e9fab7bfc..c923799d613 100644 --- a/lib/ansible/playbook/helpers.py +++ b/lib/ansible/playbook/helpers.py @@ -219,13 +219,11 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h # nested includes, and we want the include order printed correctly display.vv("statically imported: %s" % include_file) - ti_copy = task.copy(exclude_parent=True) - ti_copy._parent = block included_blocks = load_list_of_blocks( data, play=play, parent_block=None, - task_include=ti_copy, + task_include=task, role=role, use_handlers=use_handlers, loader=loader, diff --git a/lib/ansible/playbook/play.py b/lib/ansible/playbook/play.py index 5cb639f851b..c4485a6bfe1 100644 --- a/lib/ansible/playbook/play.py +++ b/lib/ansible/playbook/play.py @@ -320,7 +320,7 @@ class Play(Base, Taggable, CollectionSearch): if self.pre_tasks: b.block = self.pre_tasks else: - nt = noop_task.copy(exclude_parent=True) + nt = noop_task.copy() nt._parent = b b.block = [nt] b.always = [flush_block] @@ -331,7 +331,7 @@ class Play(Base, Taggable, CollectionSearch): if tasks: b.block = tasks else: - nt = noop_task.copy(exclude_parent=True) + nt = noop_task.copy() nt._parent = b b.block = [nt] b.always = [flush_block] @@ -341,7 +341,7 @@ class Play(Base, Taggable, CollectionSearch): if self.post_tasks: b.block = self.post_tasks else: - nt = noop_task.copy(exclude_parent=True) + nt = noop_task.copy() nt._parent = b b.block = [nt] b.always = [flush_block] @@ -385,12 +385,8 @@ class Play(Base, Taggable, CollectionSearch): return tasklist def copy(self): - new_me = super(Play, self).copy() + new_me = super().copy() new_me.role_cache = self.role_cache.copy() - new_me._included_conditional = self._included_conditional - new_me._included_path = self._included_path - new_me._action_groups = self._action_groups - new_me._group_actions = self._group_actions return new_me def _post_validate_validate_argspec(self, attr: NonInheritableFieldAttribute, value: object, templar: _TE) -> str | None: diff --git a/lib/ansible/playbook/role/__init__.py b/lib/ansible/playbook/role/__init__.py index ab79c55765b..05ec80d6093 100644 --- a/lib/ansible/playbook/role/__init__.py +++ b/lib/ansible/playbook/role/__init__.py @@ -601,10 +601,8 @@ class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable): block_list.extend(dep_blocks) for task_block in self._handler_blocks: - new_task_block = task_block.copy() - new_task_block._dep_chain = new_dep_chain - new_task_block._play = play - block_list.append(new_task_block) + task_block._dep_chain = new_dep_chain + block_list.append(task_block) return block_list @@ -642,10 +640,8 @@ class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable): block_list.extend(dep_blocks) for task_block in self._task_blocks: - new_task_block = task_block.copy() - new_task_block._dep_chain = new_dep_chain - new_task_block._play = play - block_list.append(new_task_block) + task_block._dep_chain = new_dep_chain + block_list.append(task_block) eor_block = Block(play=play) eor_block._loader = self._loader diff --git a/lib/ansible/playbook/role_include.py b/lib/ansible/playbook/role_include.py index 3c44ed340ab..3f8c5519bc1 100644 --- a/lib/ansible/playbook/role_include.py +++ b/lib/ansible/playbook/role_include.py @@ -164,15 +164,9 @@ class IncludeRole(TaskInclude): return ir - def copy(self, exclude_parent=False, exclude_tasks=False): - - new_me = super(IncludeRole, self).copy(exclude_parent=exclude_parent, exclude_tasks=exclude_tasks) - new_me.statically_loaded = self.statically_loaded + def copy(self): + new_me = super().copy() 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 def get_include_params(self): diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index 5f144f323bf..d267b83e81a 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -485,23 +485,6 @@ class Task(Base, Conditional, Taggable, CollectionSearch, Notifiable, Delegatabl all_vars |= self.vars return all_vars - def copy(self, exclude_parent: bool = False, exclude_tasks: bool = False) -> Task: - new_me = super(Task, self).copy() - - new_me._parent = None - if self._parent and not exclude_parent: - new_me._parent = self._parent.copy(exclude_tasks=exclude_tasks) - - new_me._role = None - if self._role: - new_me._role = self._role - - new_me.implicit = self.implicit - new_me._resolved_action = self._resolved_action - new_me._uuid = self._uuid - - return new_me - def set_loader(self, loader): """ Sets the loader on this object and recursively on parent, child objects. diff --git a/lib/ansible/playbook/task_include.py b/lib/ansible/playbook/task_include.py index 4bb0f2114a6..492157649f7 100644 --- a/lib/ansible/playbook/task_include.py +++ b/lib/ansible/playbook/task_include.py @@ -98,11 +98,6 @@ class TaskInclude(Task): return ds - def copy(self, exclude_parent=False, exclude_tasks=False): - new_me = super(TaskInclude, self).copy(exclude_parent=exclude_parent, exclude_tasks=exclude_tasks) - new_me.statically_loaded = self.statically_loaded - return new_me - def build_parent_block(self): """ This method is used to create the parent block for the included tasks diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py index 548a64d0e7d..2fdb06b864b 100644 --- a/lib/ansible/plugins/strategy/__init__.py +++ b/lib/ansible/plugins/strategy/__init__.py @@ -451,8 +451,7 @@ class StrategyBase: task = Task() else: - task = found_task.copy(exclude_parent=True, exclude_tasks=True) - task._parent = found_task._parent + task = found_task.copy() task.from_attrs(wire_task_result.task_fields) @@ -773,8 +772,7 @@ class StrategyBase: """ A proven safe and performant way to create a copy of an included file """ - ti_copy = included_file._task.copy(exclude_parent=True) - ti_copy._parent = included_file._task._parent + ti_copy = included_file._task.copy() temp_vars = ti_copy.vars | included_file._vars