diff --git a/changelogs/fragments/71627-add_host-group_by-fix-changed_when-in-loop.yml b/changelogs/fragments/71627-add_host-group_by-fix-changed_when-in-loop.yml new file mode 100644 index 00000000000..312e4dadd58 --- /dev/null +++ b/changelogs/fragments/71627-add_host-group_by-fix-changed_when-in-loop.yml @@ -0,0 +1,2 @@ +bugfixes: + - "add_host/group_by: fix using changed_when in a loop (https://github.com/ansible/ansible/issues/71627)" diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py index 417eec0614e..afe3936baf5 100644 --- a/lib/ansible/constants.py +++ b/lib/ansible/constants.py @@ -71,6 +71,7 @@ _ACTION_INCLUDE = add_internal_fqcns(('include', )) _ACTION_INCLUDE_ROLE = add_internal_fqcns(('include_role', )) _ACTION_INCLUDE_TASKS = add_internal_fqcns(('include_tasks', )) _ACTION_INCLUDE_VARS = add_internal_fqcns(('include_vars', )) +_ACTION_INVENTORY_TASKS = add_internal_fqcns(('add_host', 'group_by')) _ACTION_META = add_internal_fqcns(('meta', )) _ACTION_SET_FACT = add_internal_fqcns(('set_fact', )) _ACTION_SETUP = add_internal_fqcns(('setup', )) diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py index d87ab9ad7ea..d7f464f24f1 100644 --- a/lib/ansible/executor/task_executor.py +++ b/lib/ansible/executor/task_executor.py @@ -393,7 +393,8 @@ class TaskExecutor: else: if getattr(self._task, 'diff', False): self._final_q.send_callback('v2_on_file_diff', tr) - self._final_q.send_callback('v2_runner_item_on_ok', tr) + if self._task.action not in C._ACTION_INVENTORY_TASKS: + self._final_q.send_callback('v2_runner_item_on_ok', tr) results.append(res) del task_vars[loop_var] diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index 2f1f92cb6cc..47a74f738b8 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -268,6 +268,14 @@ class Task(Base, Conditional, Taggable, CollectionSearch): e.message += '\nThis error can be suppressed as a warning using the "invalid_task_attribute_failed" configuration' raise e + def _validate_changed_when(self, attr, name, value): + if not isinstance(value, list): + setattr(self, name, [value]) + + def _validate_failed_when(self, attr, name, value): + if not isinstance(value, list): + setattr(self, name, [value]) + def post_validate(self, templar): ''' Override of base class post_validate, to also do final validation on diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py index ae3d0e7d55e..bbb2b67e429 100644 --- a/lib/ansible/plugins/strategy/__init__.py +++ b/lib/ansible/plugins/strategy/__init__.py @@ -76,20 +76,37 @@ class StrategySentinel: _sentinel = StrategySentinel() -def post_process_whens(result, task, templar): - +def post_process_whens(result, task, templar, task_vars): cond = None if task.changed_when: - cond = Conditional(loader=templar._loader) - cond.when = task.changed_when - result['changed'] = cond.evaluate_conditional(templar, templar.available_variables) + with templar.set_temporary_context(available_variables=task_vars): + cond = Conditional(loader=templar._loader) + cond.when = task.changed_when + result['changed'] = cond.evaluate_conditional(templar, templar.available_variables) if task.failed_when: - if cond is None: - cond = Conditional(loader=templar._loader) - cond.when = task.failed_when - failed_when_result = cond.evaluate_conditional(templar, templar.available_variables) - result['failed_when_result'] = result['failed'] = failed_when_result + with templar.set_temporary_context(available_variables=task_vars): + if cond is None: + cond = Conditional(loader=templar._loader) + cond.when = task.failed_when + failed_when_result = cond.evaluate_conditional(templar, templar.available_variables) + result['failed_when_result'] = result['failed'] = failed_when_result + + +def _get_item_vars(result, task): + item_vars = {} + if task.loop or task.loop_with: + loop_var = result.get('ansible_loop_var', 'item') + index_var = result.get('ansible_index_var') + if loop_var in result: + item_vars[loop_var] = result[loop_var] + if index_var and index_var in result: + item_vars[index_var] = result[index_var] + if '_ansible_item_label' in result: + item_vars['_ansible_item_label'] = result['_ansible_item_label'] + if 'ansible_loop' in result: + item_vars['ansible_loop'] = result['ansible_loop'] + return item_vars def results_thread_main(strategy): @@ -680,12 +697,32 @@ class StrategyBase: # this task added a new host (add_host module) new_host_info = result_item.get('add_host', dict()) self._add_host(new_host_info, result_item) - post_process_whens(result_item, original_task, handler_templar) elif 'add_group' in result_item: # this task added a new group (group_by module) self._add_group(original_host, result_item) - post_process_whens(result_item, original_task, handler_templar) + + if 'add_host' in result_item or 'add_group' in result_item: + item_vars = _get_item_vars(result_item, original_task) + found_task_vars = self._queued_task_cache.get((original_host.name, task_result._task._uuid))['task_vars'] + if item_vars: + all_task_vars = combine_vars(found_task_vars, item_vars) + else: + all_task_vars = found_task_vars + all_task_vars[original_task.register] = wrap_var(result_item) + post_process_whens(result_item, original_task, handler_templar, all_task_vars) + if original_task.loop or original_task.loop_with: + new_item_result = TaskResult( + task_result._host, + task_result._task, + result_item, + task_result._task_fields, + ) + self._tqm.send_callback('v2_runner_item_on_ok', new_item_result) + if result_item.get('changed', False): + task_result._result['changed'] = True + if result_item.get('failed', False): + task_result._result['failed'] = True if 'ansible_facts' in result_item and original_task.action not in C._ACTION_DEBUG: # if delegated fact and we are delegating facts, we need to change target host for them diff --git a/test/integration/targets/add_host/tasks/main.yml b/test/integration/targets/add_host/tasks/main.yml index 399b0b6b419..d1583eff31e 100644 --- a/test/integration/targets/add_host/tasks/main.yml +++ b/test/integration/targets/add_host/tasks/main.yml @@ -157,3 +157,20 @@ assert: that: - badinput is failed + +- name: Add hosts in a loop + add_host: + name: 'host_{{item}}' + loop: + - 1 + - 2 + - 2 + register: add_host_loop_res + +- name: verify correct changed results + assert: + that: + - add_host_loop_res.results[0] is changed + - add_host_loop_res.results[1] is changed + - add_host_loop_res.results[2] is not changed + - add_host_loop_res is changed diff --git a/test/integration/targets/changed_when/tasks/main.yml b/test/integration/targets/changed_when/tasks/main.yml index 4f0a87479a9..bc8da712af5 100644 --- a/test/integration/targets/changed_when/tasks/main.yml +++ b/test/integration/targets/changed_when/tasks/main.yml @@ -71,3 +71,41 @@ - invalid_conditional is failed - invalid_conditional.stdout is defined - invalid_conditional.changed_when_result is contains('boomboomboom') + +- add_host: + name: 'host_{{item}}' + loop: + - 1 + - 2 + changed_when: item == 2 + register: add_host_loop_res + +- assert: + that: + - add_host_loop_res.results[0] is not changed + - add_host_loop_res.results[1] is changed + - add_host_loop_res is changed + +- group_by: + key: "test_{{ item }}" + loop: + - 1 + - 2 + changed_when: item == 2 + register: group_by_loop_res + +- assert: + that: + - group_by_loop_res.results[0] is not changed + - group_by_loop_res.results[1] is changed + - group_by_loop_res is changed + +- name: use changed in changed_when + add_host: + name: 'host_3' + changed_when: add_host_loop_res is changed + register: add_host_loop_res + +- assert: + that: + - add_host_loop_res is changed