Create a dynamic new list by using other lists in Ansible

Viewed 62

I've used includ_vars for importing a file which includes some variables as follows:

my_vars:
   - { name: "a", surname: "b", status: "c"}
   - { name: "d", surname: "e", status: "f"}
   - { name: "g", surname: "h", status: "i"}

I'm going to create a new dynamic list by my_vars and use it in get_url task:

- { url: "http://company.com/a/b/c.txt"}
- { url: "http://company.com/d/e/f.txt"}
- { url: "http://company.com/g/h/i.txt"}

- name: "include my_vars"
  include_vars:
    file: "../myvars.yml"

- name: "create new URL list"
  set_fact:
     new_list: "- {url: "http://"http://company.com/{{ my_vars.name }}/{{ my_vars.surname }}/{{ my_vars.status }}.txt
  with_items: {{ my_vars }}

- name: "get desired url"
  get_url:
      url: {{ item.url }}
      dest: /tmp/
  loop: {{ new_list }}

But it doesn't work. How can I create this new iterative list by existing variables for using in other steps?

2 Answers

Do you really need the variable new_list?

Possibly the better solution for your case

But if you only want to execute the download of the respective URLs, you can do it as follows:

- name: "include my_vars"
  include_vars:
    file: "../myvars.yml"

- name: Get file from url
  get_url:
    url: "http://company.com/{{ item.name }}/{{ item.surname }}/{{ item.status }}.txt"
    dest: "/tmp/{{ item.name }}_{{ item.surname }}_{{ item.status }}.txt"
  with_items: "{{ my_vars }}"

If you want to continue working with the downloaded files later, you could do this in the same way as with the get_url task:

- name: Remove downloaded files
  file:
    path: "/tmp/{{ item.name }}_{{ item.surname }}_{{ item.status }}.txt"
    state: absent
  with_items: "{{ my_vars }}"

Alternatively, you could download the files to an empty directory (e.g. /tmp/my_downloaded_text_files/), and then use the find module to find all files in that directory.

Solution to dynamically create the list

I found a way to create the URL list, however I don't know if that is really the better option for your use case.

- name: Create list with download URLs
  set_fact:
    new_list: "{{ ( new_list | default([]) ) + [ new_url_item ] }}"
  vars:
    new_url_item: "http://company.com/{{ item.name }}/{{ item.surname }}/{{ item.status }}.txt"
  with_items: "{{ my_vars }}"

You iterate over your list of dicts via with_items. As task variable you prepare the new entry (new_url_item). This new entry is then concatenated to the existing list in each iteration.

Join the values

my_vals: "{{ my_vars|json_query('[].[name, surname, status]')|
                     map('join', '/')|
                     list }}"

gives the list of the fragments

my_vals:
  - a/b/c
  - d/e/f
  - g/h/i

Use the filter product to combine the fragments. Flatten and join the lists

my_urls: "{{ ['http://company.com/']|product(my_vals)|
                                     product(['.txt'])|
                                     map('flatten')|
                                     map('join')|
                                     list }}"

gives

my_urls:
  - http://company.com/a/b/c.txt
  - http://company.com/d/e/f.txt
  - http://company.com/g/h/i.txt

Use the filter community.general.dict_kv if you need the list of the dictionaries

my_urls_dict: "{{ my_urls|map('community.general.dict_kv', 'url')|
                          list }}"

gives

my_urls_dict:
  - {url: 'http://company.com/a/b/c.txt'}
  - {url: 'http://company.com/d/e/f.txt'}
  - {url: 'http://company.com/g/h/i.txt'}

Example of a complete playbook

- hosts: localhost

  vars:
    my_vars:
      - {name: "a", surname: "b", status: "c"}
      - {name: "d", surname: "e", status: "f"}
      - {name: "g", surname: "h", status: "i"}
    my_vals: "{{ my_vars|json_query('[].[name, surname, status]')|
                         map('join', '/')|
                         list }}"
    my_urls: "{{ ['http://company.com/']|product(my_vals)|
                                         product(['.txt'])|
                                         map('flatten')|
                                         map('join')|
                                         list }}"
    my_urls_dict: "{{ my_urls|map('community.general.dict_kv', 'url')|
                              list }}"

  tasks:
    - debug:
        var: my_urls
    - debug:
        var: my_urls_dict

Q: "How can I create a loop on objects with status: 'f' ?"

A: Select the item. For example,

    - debug:
        var: item
      loop: "{{ my_vars|selectattr('status', 'eq', 'f') }}"

gives

TASK [debug] ************************************************************
ok: [localhost] => (item={'name': 'd', 'surname': 'e', 'status': 'f'}) => 
  ansible_loop_var: item
  item:
    name: d
    status: f
    surname: e

But this answer seems to be too obvious. You're probably asking how to select such an item when the url is created. There are many options:

  • For example, combine the dictionaries
my_urls_dict: "{{ my_urls|map('community.general.dict_kv', 'url')|
                          zip(my_vars)|
                          map('combine')|
                          list }}"

gives

my_urls_dict:
  - {name: a, status: c, surname: b, url: 'http://company.com/a/b/c.txt'}
  - {name: d, status: f, surname: e, url: 'http://company.com/d/e/f.txt'}
  - {name: g, status: i, surname: h, url: 'http://company.com/g/h/i.txt'}

Then, the iteration is as trivial as before.

    - debug:
        var: item
      loop: "{{ my_urls_dict|selectattr('status', 'eq', 'f') }}"
  • The next option is the iteration of both lists together
    - debug:
        var: item
      with_together:
        - "{{ my_vars }}"
        - "{{ my_urls }}"
      when: item.0.status == 'f'
  • The trivial option would be the concatenation of the url in the loop
    - debug:
        var: url
      loop: "{{ my_vars|selectattr('status', 'eq', 'f') }}"
      vars:
        url: "http://company.com/{{ item.name }}/{{ item.surname }}/{{ item.status }}.txt"
Related