70

I'm currently using Ansible 1.7.2. I have the following test playbook:

---
- hosts: localhost
  tasks:
  - name: set fact 1
    set_fact: foo="[ 'zero' ]"

  - name: set fact 2
    set_fact: foo="{{ foo }} + [ 'one' ]"

  - name: set fact 3
    set_fact: foo="{{ foo }} + [ 'two', 'three' ]"

  - name: set fact 4
    set_fact: foo="{{ foo }} + [ '{{ item }}' ]"
    with_items:
      - four
      - five
      - six

  - debug: var=foo

The first task sets a fact that's a list with one item in it. The subsequent tasks append to that list with more values. The first three tasks work as expected, but the last one doesn't. Here's the output when I run this:

PLAY [localhost] **************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [set fact 1] ************************************************************
ok: [localhost]

TASK: [set fact 2] ************************************************************
ok: [localhost]

TASK: [set fact 3] ************************************************************
ok: [localhost]

TASK: [set fact 4] ************************************************************
ok: [localhost] => (item=four)
ok: [localhost] => (item=five)
ok: [localhost] => (item=six)

TASK: [debug var=foo] *********************************************************
ok: [localhost] => {
    "foo": [
        "zero",
        "one",
        "two",
        "three",
        "six"
    ]
}

PLAY RECAP ********************************************************************
localhost                  : ok=6    changed=0    unreachable=0    failed=0

Given the with_items in task 4 and the fact that the output shows the task properly iterated over the items in that list, I would have expected the result to contain all the numbers zero through six. But that last task seems to only be evaluating set_fact with the last item in the list. Is this possibly a bug in Ansible?

Edit: I also just tested this on ansible 1.8 and the output was identical.

Bruce P
  • 19,995
  • 8
  • 63
  • 73
  • My best guess would be that ansible evaluates `{{ foo }}` only once during the execution of task `set fact 4`. Do you wanna figure out how to merge two lists or just curious? – Kashyap Apr 01 '15 at 20:04
  • Yeah, that seems to be the case. No, not trying to just merge lists. I'm trying to keep track of dynamically generated filenames so other tasks can iterate through them. – Bruce P Apr 01 '15 at 20:56
  • Looks like this is a feature a lot of folks desire, and there's even a [pull request](https://github.com/ansible/ansible/pull/8019) for it, but it keeps getting pushed out for some reason... – Bruce P Apr 01 '15 at 21:16
  • 1
    my experience has taught me: Do all variable manipulation outside ansible. You should post the link to pull request as an answer and accept/close. – Kashyap Apr 02 '15 at 15:40
  • Your code above works as expected with ansible version 2.1.1.0. So I think they fixed things perhaps in version 2. – Victor Roetman Oct 19 '16 at 13:50
  • you could have simplified your question greatly to show a bare-minimum and easily comprehensible question. Although I do appreciate your question, coming back to it on a different day it gave me a headache to regrok. – papiro Sep 18 '20 at 16:52

7 Answers7

90

There is a workaround which may help. You may "register" results for each set_fact iteration and then map that results to list:

---
- hosts: localhost
  tasks:
  - name: set fact
    set_fact: foo_item="{{ item }}"
    with_items:
      - four
      - five
      - six
    register: foo_result

  - name: make a list
    set_fact: foo="{{ foo_result.results | map(attribute='ansible_facts.foo_item') | list }}"

  - debug: var=foo

Output:

< TASK: debug var=foo >
 ---------------------
    \   ^__^
     \  (oo)\_______
        (__)\       )\/\
            ||----w |
            ||     ||


ok: [localhost] => {
    "var": {
        "foo": [
            "four", 
            "five", 
            "six"
        ]
    }
}
serge
  • 1,104
  • 6
  • 6
  • 2
    this wil fail on ansible v2. Any ideas how to make it work on V2? – DomaNitro Feb 09 '16 at 00:55
  • It seems that this breaks if you add a `when` to your play. Instead, you'll get a `results` that just tells you that something got skipped. :-/ – Carlos Nunez Feb 26 '16 at 02:44
  • 2
    In my case I had to test to see whether the attribute existed as well. This can be achieved with `foo_result.results | selectattr('ansible_facts', 'defined') | map(attribute='ansible_facts.foo_item') | list` – Dan May 13 '16 at 03:12
23

As mentioned in other people's comments, the top solution given here was not working for me in Ansible 2.2, particularly when also using with_items.

It appears that OP's intended approach does work now with a slight change to the quoting of item.

- set_fact: something="{{ something + [ item ] }}"
  with_items:
    - one
    - two
    - three

And a longer example where I've handled the initial case of the list being undefined and added an optional when because that was also causing me grief:

- set_fact: something="{{ something|default([]) + [ item ] }}"
  with_items:
    - one
    - two
    - three
  when: item.name in allowed_things.item_list
Andrew H
  • 453
  • 1
  • 3
  • 8
stacyhorton
  • 586
  • 4
  • 5
15

I was hunting around for an answer to this question. I found this helpful. The pattern wasn't apparent in the documentation for with_items.

https://github.com/ansible/ansible/issues/39389

- hosts: localhost
  connection: local
  gather_facts: no

  tasks:
    - name: set_fact
      set_fact:
        foo: "{{ foo }} + [ '{{ item }}' ]"
      with_items:
        - "one"
        - "two"
        - "three"
      vars:
        foo: []

    - name: Print the var
      debug:
        var: foo
Grant Strachan
  • 151
  • 1
  • 3
7

Jinja 2.6 does not have the map function. So an alternate way of doing this would be:

set_fact: foo="{% for i in bar_result.results %}{{ i.ansible_facts.foo_item }}{%endfor%}"
slm
  • 15,396
  • 12
  • 109
  • 124
Russ Huguley
  • 766
  • 3
  • 9
  • 13
5

Below works for me:

- name: set fact
  set_fact: 
    foo_item: "{{foo_item | default([]) + [item]}}" 
  loop: 
    - four
    - five
    - six
2

Updated 2018-06-08: My previous answer was a bit of hack so I have come back and looked at this again. This is a cleaner Jinja2 approach.

- name: Set fact 4
  set_fact:
    foo: "{% for i in foo_result.results %}{% do foo.append(i) %}{% endfor %}{{ foo }}"

I am adding this answer as current best answer for Ansible 2.2+ does not completely cover the original question. Thanks to Russ Huguley for your answer this got me headed in the right direction but it left me with a concatenated string not a list. This solution gets a list but becomes even more hacky. I hope this gets resolved in a cleaner manner.

- name: build foo_string
  set_fact:
    foo_string: "{% for i in foo_result.results %}{{ i.ansible_facts.foo_item }}{% if not loop.last %},{% endif %}{%endfor%}"

- name: set fact foo
  set_fact:
    foo: "{{ foo_string.split(',') }}"
  • n.b. A prerequisite for this to work is to have the `jinja2.ext.do` extension enabled e.g. by adding `jinja2_extensions = jinja2.ext.do` to the `[defaults]` section of your `ansible.cfg` – Tim Small Oct 06 '20 at 08:21
0

Looks like this behavior is how Ansible currently works, although there is a lot of interest in fixing it to work as desired. There's currently a pull request with the desired functionality so hopefully this will get incorporated into Ansible eventually.

Bruce P
  • 19,995
  • 8
  • 63
  • 73