60
votes

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.

6
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
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
Looks like this is a feature a lot of folks desire, and there's even a pull request for it, but it keeps getting pushed out for some reason...Bruce P
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
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

6 Answers

87
votes

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"
        ]
    }
}
15
votes

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
9
votes

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
7
votes

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%}"
2
votes

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(',') }}"
1
votes

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.