Unexpected behavior change in loop over debug task in 2.14.2 -> 2.16 upgrade

My development team has used “complex variables” (i.e., references to specific dictionary fields) in loops over the debug task for a long time. Example:

- debug:
     var: "{{ item }}"
  with_items:
  - result.rc
  - result.stdout
  - result.stderr

We have done this in order to avoid dumping the whole dictionary, which can be much longer. Recently, we were unexpectedly upgraded from Ansible 2.14.2 to Ansible 2.16. We find that this behavior no longer works. Instead of printing the values for each field in the list, it now prints something that looks like a recursive reference error (i.e., “dict.field” is displayed as “{{ dict.field }}”).

A simple playbook (shown below, with output from both versions) demonstrates the change in behavior. (While trying to find a workaround, we have found that replacing “with_items” with “loop” gives us a workaround in both versions, but we’re not sure why.)

This happens even if we just put the dictionary variable as a single entry in the loop.

Is the use of a dictionary field (instead of just a plain, undecorated variable name) allowed in the debug task? Can loops be applied to the debug task? Is the change in behavior expected?

Thank you.

Playbook:

- hosts: installer
  gather_facts: false
  vars:
    result:
      rc: 0
      stdout: 'TASK OUTPUT'
      stderr: 'ERROR OUTPUT'
  tasks:
  - debug:
      var: "{{ item }}"
    with_items:
    - result.rc
    - result.stdout
    - result.stderr

  - debug:
      var: "{{ item }}"
    loop:
    - result.rc
    - result.stdout
    - result.stderr

Results on 2.14.2 (got what I expected)

PLAY [installer] ***************************************************************

TASK [debug] *******************************************************************
ok: [paas-installer] => (item=result.rc) => {
    "ansible_loop_var": "item",
    "item": "result.rc",
    "result.rc": "0"
}
ok: [paas-installer] => (item=result.stdout) => {
    "ansible_loop_var": "item",
    "item": "result.stdout",
    "result.stdout": "TASK OUTPUT"
}
ok: [paas-installer] => (item=result.stderr) => {
    "ansible_loop_var": "item",
    "item": "result.stderr",
    "result.stderr": "ERROR OUTPUT"
}

TASK [debug] *******************************************************************
ok: [paas-installer] => (item=result.rc) => {
    "ansible_loop_var": "item",
    "item": "result.rc",
    "result.rc": "0"
}
ok: [paas-installer] => (item=result.stdout) => {
    "ansible_loop_var": "item",
    "item": "result.stdout",
    "result.stdout": "TASK OUTPUT"
}
ok: [paas-installer] => (item=result.stderr) => {
    "ansible_loop_var": "item",
    "item": "result.stderr",
    "result.stderr": "ERROR OUTPUT"
}

PLAY RECAP *********************************************************************
paas-installer             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Results on 2.16 (first debug task has unexpected results):

PLAY [installer] ***************************************************************

TASK [debug] *******************************************************************
ok: [paas-installer] => (item=result.rc) => {
    "ansible_loop_var": "item",
    "item": "result.rc",
    "result.rc": "{{result.rc}}"
}
ok: [paas-installer] => (item=result.stdout) => {
    "ansible_loop_var": "item",
    "item": "result.stdout",
    "result.stdout": "{{result.stdout}}"
}
ok: [paas-installer] => (item=result.stderr) => {
    "ansible_loop_var": "item",
    "item": "result.stderr",
    "result.stderr": "{{result.stderr}}"
}

TASK [debug] *******************************************************************
ok: [paas-installer] => (item=result.rc) => {
    "ansible_loop_var": "item",
    "item": "result.rc",
    "result.rc": "0"
}
ok: [paas-installer] => (item=result.stdout) => {
    "ansible_loop_var": "item",
    "item": "result.stdout",
    "result.stdout": "TASK OUTPUT"
}
ok: [paas-installer] => (item=result.stderr) => {
    "ansible_loop_var": "item",
    "item": "result.stderr",
    "result.stderr": "ERROR OUTPUT"
}

PLAY RECAP *********************************************************************
paas-installer : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Version details:
2.14.2 installation

ansible [core 2.14.2]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/var/paas/tmp/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.11/site-packages/ansible
  ansible collection location = /var/paas/tmp/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.11.5 (main, Sep 22 2023, 15:34:29) [GCC 8.5.0 20210514 (Red Hat 8.5.0-20)] (/usr/bin/python3.11)
  jinja version = 3.1.2
  libyaml = True

Upgraded 2.16 installation

ansible [core 2.16.3]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/var/paas/tmp/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.12/site-packages/ansible
  ansible collection location = /var/paas/tmp/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.12.1 (main, Feb 21 2024, 10:25:11) [GCC 8.5.0 20210514 (Red Hat 8.5.0-21)] (/usr/bin/python3.12)
  jinja version = 3.1.2
  libyaml = True

It’s best to use loop instead of with_items for looping over a list, because it involves less magic. with_items: ['foo'] is the equivalent of loop: "{{ query('items', ['foo']) }}", which is itself surprising to most people and can have surprising behaviour. The change in behaviour is due to the security fix that made it harder for data marked as unsafe to be evaluated. The result of lookups (like items) is marked as unsafe by default, so with_items transforms your list into a list of unsafe strings.

It’s also best to avoid embedded templates entirely, and more efficient to use a non-loop construct here.

  - debug:
      msg:
        rc: "{{ result.rc }}"
        stdout: "{{ result.stdout }}"
        stderr: "{{ result.stderr }}"
TASK [debug] *******************************************************************
ok: [localhost] =>
    msg:
        rc: 0
        stderr: ERROR OUTPUT
        stdout: TASK OUTPUT
2 Likes

Wow. I had seen that information, but didn’t understand how it applied to my situation. I appreciate the detail here, including the backward compatibility and security detail link. I couldn’t have asked for a better reply. Thank you.

1 Like