From 9007dbec2fe96d827bd1b713df6b4c1a2a51dc88 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Fri, 7 Dec 2018 13:49:50 -0600 Subject: [PATCH] Add info about loop based on jinja2 loop var (#42134) * Add info about loop based on jinja2 loop var * ansible_loop * Update test count * Add extended loop_control that defines whether ansible_loop should be added * Extended needs to be defaulted * Revert "Update test count" This reverts commit f1e93ee469825f4cdcd90fb28667d29aa088275c. * Add docs about loop_control.extended * Add revindex and revindex0 * Document ansible_loop in special vars * Add changelog fragment * Add tests, change items to allitems so that dot notation works, fix logic error with previtem --- changelogs/fragments/loop-info.yaml | 2 + .../special_variables.rst | 3 + .../rst/user_guide/playbooks_loops.rst | 24 +++++++ lib/ansible/executor/task_executor.py | 24 +++++++ lib/ansible/playbook/loop_control.py | 1 + test/integration/targets/loops/tasks/main.yml | 63 +++++++++++++++++++ 6 files changed, 117 insertions(+) create mode 100644 changelogs/fragments/loop-info.yaml diff --git a/changelogs/fragments/loop-info.yaml b/changelogs/fragments/loop-info.yaml new file mode 100644 index 0000000000..0536f3a203 --- /dev/null +++ b/changelogs/fragments/loop-info.yaml @@ -0,0 +1,2 @@ +minor_changes: +- loop_control - Add new ``extended`` option to return extended loop information (https://github.com/ansible/ansible/pull/42134) diff --git a/docs/docsite/rst/reference_appendices/special_variables.rst b/docs/docsite/rst/reference_appendices/special_variables.rst index 4d3be0f3e6..013a30814e 100644 --- a/docs/docsite/rst/reference_appendices/special_variables.rst +++ b/docs/docsite/rst/reference_appendices/special_variables.rst @@ -25,6 +25,9 @@ ansible_inventory_sources ansible_limit Contents of the ``--limit`` CLI option for the current execution of Ansible +ansible_loop + A dictionary/map containing extended loop information when enabled via ``loop_control.extended`` + ansible_play_batch List of active hosts in the current play run limited by the serial, aka 'batch'. Failed/Unreachable hosts are not considered 'active'. diff --git a/docs/docsite/rst/user_guide/playbooks_loops.rst b/docs/docsite/rst/user_guide/playbooks_loops.rst index 367bd8100a..a120292303 100644 --- a/docs/docsite/rst/user_guide/playbooks_loops.rst +++ b/docs/docsite/rst/user_guide/playbooks_loops.rst @@ -330,6 +330,30 @@ If you need to keep track of where you are in a loop, you can use the ``index_va loop_control: index_var: my_idx +.. versionadded:: 2.8 + +As of Ansible 2.8 you can get extended loop information using the ``extended`` option to loop control. This option will expose the following information. + +========================== =========== +Variable Description +-------------------------- ----------- +``ansible_loop.allitems`` The list of all items in the loop +``ansible_loop.index`` The current iteration of the loop. (1 indexed) +``ansible_loop.index0`` The current iteration of the loop. (0 indexed) +``ansible_loop.revindex`` The number of iterations from the end of the loop (1 indexed) +``ansible_loop.revindex0`` The number of iterations from the end of the loop (0 indexed) +``ansible_loop.first`` ``True`` if first iteration +``ansible_loop.last`` ``True`` if last iteration +``ansible_loop.length`` The number of items in the loop +``ansible_loop.previtem`` The item from the previous iteration of the loop. Undefined during the first iteration. +``ansible_loop.nextitem`` The item from the following iteration of the loop. Undefined during the last iteration. +========================== =========== + +:: + + loop_control: + extended: yes + Migrating from with_X to loop ````````````````````````````` diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py index af758ee3eb..a6b72fb87c 100644 --- a/lib/ansible/executor/task_executor.py +++ b/lib/ansible/executor/task_executor.py @@ -287,6 +287,7 @@ class TaskExecutor: index_var = None label = None loop_pause = 0 + extended = False templar = Templar(loader=self._loader, shared_loader_obj=self._shared_loader_obj, variables=self._job_vars) # FIXME: move this to the object itself to allow post_validate to take care of templating (loop_control.post_validate) @@ -294,6 +295,7 @@ class TaskExecutor: loop_var = templar.template(self._task.loop_control.loop_var) index_var = templar.template(self._task.loop_control.index_var) loop_pause = templar.template(self._task.loop_control.pause) + extended = templar.template(self._task.loop_control.extended) # This may be 'None',so it is tempalted below after we ensure a value and an item is assigned label = self._task.loop_control.label @@ -313,11 +315,30 @@ class TaskExecutor: items = self._squash_items(items, loop_var, task_vars) no_log = False + items_len = len(items) for item_index, item in enumerate(items): task_vars[loop_var] = item if index_var: task_vars[index_var] = item_index + if extended: + task_vars['ansible_loop'] = { + 'allitems': items, + 'index': item_index + 1, + 'index0': item_index, + 'first': item_index == 0, + 'last': item_index + 1 == items_len, + 'length': items_len, + 'revindex': items_len - item_index, + 'revindex0': items_len - item_index - 1, + } + try: + task_vars['ansible_loop']['nextitem'] = items[item_index + 1] + except IndexError: + pass + if item_index - 1 >= 0: + task_vars['ansible_loop']['previtem'] = items[item_index - 1] + # Update template vars to reflect current loop iteration templar.set_available_variables(task_vars) @@ -355,6 +376,9 @@ class TaskExecutor: res[loop_var] = item if index_var: res[index_var] = item_index + if extended: + res['ansible_loop'] = task_vars['ansible_loop'] + res['_ansible_item_result'] = True res['_ansible_ignore_errors'] = task_fields.get('ignore_errors') diff --git a/lib/ansible/playbook/loop_control.py b/lib/ansible/playbook/loop_control.py index 366d9cdaa4..9e3cbb76a8 100644 --- a/lib/ansible/playbook/loop_control.py +++ b/lib/ansible/playbook/loop_control.py @@ -29,6 +29,7 @@ class LoopControl(FieldAttributeBase): _index_var = FieldAttribute(isa='str') _label = FieldAttribute(isa='str') _pause = FieldAttribute(isa='int', default=0) + _extended = FieldAttribute(isa='bool') def __init__(self): super(LoopControl, self).__init__() diff --git a/test/integration/targets/loops/tasks/main.yml b/test/integration/targets/loops/tasks/main.yml index c3a169f542..c46c165d99 100644 --- a/test/integration/targets/loops/tasks/main.yml +++ b/test/integration/targets/loops/tasks/main.yml @@ -268,3 +268,66 @@ things: - !unsafe foo - !unsafe bar + +- name: extended loop info + assert: + that: + - ansible_loop.nextitem == 'orange' + - ansible_loop.index == 1 + - ansible_loop.index0 == 0 + - ansible_loop.first + - not ansible_loop.last + - ansible_loop.previtem is undefined + - ansible_loop.allitems == ['apple', 'orange', 'banana'] + - ansible_loop.revindex == 3 + - ansible_loop.revindex0 == 2 + - ansible_loop.length == 3 + loop: + - apple + - orange + - banana + loop_control: + extended: true + when: item == 'apple' + +- name: extended loop info 2 + assert: + that: + - ansible_loop.nextitem == 'banana' + - ansible_loop.index == 2 + - ansible_loop.index0 == 1 + - not ansible_loop.first + - not ansible_loop.last + - ansible_loop.previtem == 'apple' + - ansible_loop.allitems == ['apple', 'orange', 'banana'] + - ansible_loop.revindex == 2 + - ansible_loop.revindex0 == 1 + - ansible_loop.length == 3 + loop: + - apple + - orange + - banana + loop_control: + extended: true + when: item == 'orange' + +- name: extended loop info 3 + assert: + that: + - ansible_loop.nextitem is undefined + - ansible_loop.index == 3 + - ansible_loop.index0 == 2 + - not ansible_loop.first + - ansible_loop.last + - ansible_loop.previtem == 'orange' + - ansible_loop.allitems == ['apple', 'orange', 'banana'] + - ansible_loop.revindex == 1 + - ansible_loop.revindex0 == 0 + - ansible_loop.length == 3 + loop: + - apple + - orange + - banana + loop_control: + extended: true + when: item == 'banana'