'creating dictionary with ansible

I want to create a dictionary in ansible from a list; using some variables for the value in the key-value pair of the dictionary, but it seems to be not working.

I've simplified just to the problem and created a sample playbook to reproduce the issue, can someone help me out. Thanks!

here is my playbook

---
- name: create dictionary test
  hosts: all
  connection: local
  gather_facts: False

  vars:
    ports: [80, 443]
    server_base: "org.com"

  tasks:
    - name: print the ports
      debug:
        msg: "ports: {{ports}}"

    - name: create a dictionary
      set_fact:
        #server_rules: "{{server_rules|default([]) + [{'server': '{{server_base}}-{{item}}', 'port': item}]}}"
        server_rules: "{{server_rules|default([]) + [{'server': '{{server_base}}', 'port': item}]}}"
      loop: "{{ports|flatten(1)}}"

    - name: output
      debug:
        msg: "server_rules: {{server_rules}}"

With the above it works, the output as below:

$ansible-playbook -i "localhost," dicttest.yaml

PLAY [create dictionary test] ***************************************************************************************************************************************

TASK [print the ports] **********************************************************************************************************************************************
ok: [localhost] => {
    "msg": "ports: [80, 443]"
}

TASK [create a dictionary] ******************************************************************************************************************************************
ok: [localhost] => (item=80)
ok: [localhost] => (item=443)

TASK [output] *******************************************************************************************************************************************************
ok: [localhost] => {
    "msg": "server_rules: [{'server': 'org.com', 'port': 80}, {'server': 'org.com', 'port': 443}]"
}

PLAY RECAP **********************************************************************************************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

But when I change set fact to (uncomment the one commented line and comment the other one)

server_rules: "{{server_rules|default([]) + [{'server': '{{server_base}}-{{item}}', 'port': item}]}}"

it fails with the following error

$ansible-playbook -i "localhost," dicttest.yaml

PLAY [create dictionary test] ***************************************************************************************************************************************

TASK [print the ports] **********************************************************************************************************************************************
ok: [localhost] => {
    "msg": "ports: [80, 443]"
}

TASK [create a dictionary] ******************************************************************************************************************************************
ok: [localhost] => (item=80)
ok: [localhost] => (item=443)

TASK [output] *******************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'item' is undefined\n\nThe error appears to be in '/Users/dev/dicttest.yaml': line 22, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n    - name: output\n      ^ here\n"}

PLAY RECAP **********************************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

Can someone explain how to get this working.



Solution 1:[1]

Q: Given the data

  ports: [80, 443]
  server_base: org.com

create a list of dictionaries

  server_rules:
  - port: 80
    server: org.com-80
  - port: 443
    server: org.com-443

A: The task below gives the expected result

    - name: create a list of dictionaries
      set_fact:
        server_rules: "{{ server_rules|default([]) +
                          [{'server': server_base  + '-' + item|string,
                            'port': item}] }}"
      loop: "{{ ports }}"

It's not necessary to iterate the list in a task. The declaration of the variables below does the same job

  servers: "{{ [server_base]|
               product(ports)|
               map('join', '-')|
               map('community.general.dict_kv', 'server')|
               list }}"
  server_rules: "{{ servers|
                    zip(ports|map('community.general.dict_kv', 'port'))|
                    map('combine')|
                    list }}"

Notes:

  • Double braces "{{ }}" can't be nested. The expression below is wrong
      "{{ var1 + ['{{server_base}}-'] }}"

Correct

      "{{ var1 + [server_base + '-'] }}"

In YAML, the operator plus "+" is used both to concatenate strings and lists. This is because a string in YAML is technically a list of characters. It's recommended to use "~" to concatenate strings

Also correct

      "{{ var1 + [server_base ~ '-'] }}"
  • Use var attribute in debug. The output is more readable with stdout_callback = yaml
    - debug:
        var: server_rules
  • The created variable server_rules is a list. The items are dictionaries. Hence, it's a list of dictionaries.

  • The variable ports is a simple list. There is no need to use the filter flatten.

  • The combination of "hosts: all" and "connection: local" would make to run all hosts at the localhost

  hosts: all
  connection: local

Use "hosts: localhost" if you want to run the playbook at the localhost. In this case "connection: local" is the default

  hosts: localhost

If you want to run a task at the localhost, but still want the play to read the variables for all hosts, use "delegate_to: localhost" and limit the task to "run_once: true". For example

  - hosts: all
    tasks:
      - copy:
          content: "{{ ansible_play_hosts|to_nice_yaml }}"
          dest: /tmp/ansible_play_hosts.yml
        delegate_to: localhost
        run_once: true

Solution 2:[2]

got an answer from another person outside of stackoverflow, posting here just to make sure anyone looking at this, gets the solution as well

changing as follows fixes the problem

server_rules: "{{server_rules|default([]) + [{'server': '{{server_base}}-' + item|string, 'port': item}]}}"

The complete working playbook

---
- name: create dictionary test
  hosts: all
  connection: local
  gather_facts: False

  vars:
    ports: [80, 443]
    server_base: "org.com"

  tasks:
    - name: print the ports
      debug:
        msg: "ports: {{ports}}"

    - name: create a dictionary
      set_fact:
        server_rules: "{{server_rules|default([]) + [{'server': '{{server_base}}-' + item|string, 'port': item}]}}"
      loop: "{{ports|flatten(1)}}"

    - name: output
      debug:
        msg: "server_rules: {{server_rules}}"

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 xbalaji