Using set_facts and with_items together in Ansible
Asked Answered
D

7

72

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.

Dragoman answered 1/4, 2015 at 19:52 Comment(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?Lawrenson
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.Dragoman
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...Dragoman
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.Lawrenson
Your code above works as expected with ansible version 2.1.1.0. So I think they fixed things perhaps in version 2.Duluth
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.Chancellery
D
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.

Dragoman answered 2/4, 2015 at 17:36 Comment(0)
J
91

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"
        ]
    }
}
Jawbreaker answered 23/4, 2015 at 8:17 Comment(3)
this wil fail on ansible v2. Any ideas how to make it work on V2?Circumambient
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. :-/Arleen
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') | listPodiatry
R
27

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
Redemptioner answered 13/11, 2017 at 3:56 Comment(0)
B
16

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
Bettiebettina answered 30/7, 2019 at 11:44 Comment(0)
B
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%}"
Ba answered 15/6, 2015 at 18:43 Comment(0)
L
6

Below works for me:

- name: set fact
  set_fact: 
    foo_item: "{{foo_item | default([]) + [item]}}" 
  loop: 
    - four
    - five
    - six
Lasser answered 20/9, 2021 at 1:49 Comment(0)
H
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(',') }}"
Hostage answered 22/5, 2018 at 19:40 Comment(1)
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.cfgTimepleaser
D
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.

Dragoman answered 2/4, 2015 at 17:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.