'How to create a list of variables per inventory host in ansible

I have following situation.
I have inventory like this (limited to relevant information):

[management]
management-1 vhost_ip=10.0.0.1

[database]
db-1 vhost_ip=10.0.1.1
db-2 vhost_ip=10.0.1.2

[application]
app-1 vhost_ip=10.0.2.1
app-2 vhost_ip=10.0.2.2

Now I need to create a play like this:

- name: Setup management server
  hosts: management
  become: true
  roles:
    - management
      database_ips:
        - 10.0.1.1
        - 10.0.1.1
      application_ips:
        - 10.0.2.1
        - 10.0.2.1

As you can see my management role need a list of all IPs grouped by role.
I do have those IPs already available inside my inventory.

Is there any way to do a conversion that will extract those IPs and turn them into a list? So I could call it something like this (pseudocode based on laravels higher order lists):

  roles:
    - management
      database_ips: {{ groups['databse']->map->vhost_ip }}
      application_ips: {{ groups['application']->map->vhost_ip }}

Also is it possible to do something similar but with a more complex format like this (mapping vhost_ip from inventory to ip in parameter and alias from inventory to name in parameter):

  roles:
    - management
      database_hosts:
        - name: db-1
          ip: 10.0.1.1
        - name: db-2
          ip: 10.0.1.1


Solution 1:[1]

Yes, this is pretty easy to achieve with the right set of filters applied to the special variable hostvars.

The set of filter we want to apply are:

  • dict2items so we can make a list of the hostvars dictionary in order to filter on it
  • selectattr in order to filter the nodes that are in a specific, with another special variables, groups
  • And finally a map to extract one attribute from each dictionary into a simple list

So, we end up with those variables:

application_ips: >-
  {{
    hostvars  
    | dict2items 
    | selectattr('key', 'in', groups.application) 
    | map(attribute='value.vhost_ip')
  }} 
database_ips: >-
  {{
    hostvars  
    | dict2items 
    | selectattr('key', 'in', groups.database) 
    | map(attribute='value.vhost_ip')
  }}
management_ips: >-
  {{
    hostvars  
    | dict2items 
    | selectattr('key', 'in', groups.management) 
    | map(attribute='value.vhost_ip')
  }}

As for the more complex requirement, you can use a json_query and a JMESPath query.

application_ips: >-
  {{
    hostvars  
    | dict2items 
    | selectattr('key', 'in', groups.application) 
    | json_query('[].{name: key, ip: value.vhost_ip}')
  }} 
database_ips: >-
  {{
    hostvars  
    | dict2items 
    | selectattr('key', 'in', groups.database) 
    | json_query('[].{name: key, ip: value.vhost_ip}')
  }}
management_ips: >-
  {{
    hostvars  
    | dict2items 
    | selectattr('key', 'in', groups.management) 
    | json_query('[].{name: key, ip: value.vhost_ip}')
  }}

Given the playbook:

- hosts: all
  gather_facts: no

  tasks:
    - debug:
        msg: 
          application_ips: "{{ application_ips }}"
          database_ips: "{{ database_ips }}"
          management_ips: "{{ management_ips }}"
      vars:
        application_ips: >-
          {{
            hostvars  
            | dict2items 
            | selectattr('key', 'in', groups.application) 
            | map(attribute="value.vhost_ip")
          }} 
        database_ips: >-
          {{
            hostvars  
            | dict2items 
            | selectattr('key', 'in', groups.database) 
            | map(attribute="value.vhost_ip")
          }}
        management_ips: >-
          {{
            hostvars  
            | dict2items 
            | selectattr('key', 'in', groups.management) 
            | map(attribute="value.vhost_ip")
          }}
      run_once: true
      delegate_to: localhost

    - debug:
        msg: 
          application_ips: "{{ application_ips }}"
          database_ips: "{{ database_ips }}"
          management_ips: "{{ management_ips }}"
      vars:
        application_ips: >-
          {{
            hostvars  
            | dict2items 
            | selectattr('key', 'in', groups.application) 
            | json_query('[].{name: key, ip: value.vhost_ip}')
          }} 
        database_ips: >-
          {{
            hostvars  
            | dict2items 
            | selectattr('key', 'in', groups.database) 
            | json_query('[].{name: key, ip: value.vhost_ip}')
          }}
        management_ips: >-
          {{
            hostvars  
            | dict2items 
            | selectattr('key', 'in', groups.management) 
            | json_query('[].{name: key, ip: value.vhost_ip}')
          }}
      run_once: true
      delegate_to: localhost

This yields:

TASK [debug] ******************************************************
ok: [node2 -> localhost] => 
  msg:
    application_ips:
    - 10.0.2.1
    - 10.0.2.2
    database_ips:
    - 10.0.1.1
    - 10.0.1.2
    management_ips:
    - 10.0.0.1

TASK [debug] ******************************************************
ok: [node2 -> localhost] => 
  msg:
    application_ips:
    - ip: 10.0.2.1
      name: app-1
    - ip: 10.0.2.2
      name: app-2
    database_ips:
    - ip: 10.0.1.1
      name: db-1
    - ip: 10.0.1.2
      name: db-2
    management_ips:
    - ip: 10.0.0.1
      name: management-1

Solution 2:[2]

Iterate the groups and create a dictionary

  - set_fact:
      d1: "{{ d1|d({})|
              combine({item: dict(_keys|zip(_vals))}) }}"
    loop: "{{ groups.keys()|list|difference(['all', 'ungrouped']) }}"
    vars:
      _keys: "{{ groups[item] }}"
      _vals: "{{ groups[item]|
                 map('extract', hostvars, 'vhost_ip')|
                 list }}"
    run_once: true

gives

  d1:
    application:
      app-1: 10.0.2.1
      app-2: 10.0.2.2
    database:
      db-1: 10.0.1.1
      db-2: 10.0.1.2
    management:
      management-1: 10.0.0.1

The selection of the lists is trivial now

  database_ips: "{{ d1.database.values()|list }}"
  application_ips: "{{ d1.application.values()|list }}"

gives

  database_ips:
    - 10.0.1.1
    - 10.0.1.2

  application_ips:
    - 10.0.2.1
    - 10.0.2.2

The dictionary d1 can be also used to create the variable database_hosts

  database_hosts: "{{ d1.database|
                      dict2items(key_name='name', value_name='ip') }}"

gives

  database_hosts:
    - ip: 10.0.1.1
      name: db-1
    - ip: 10.0.1.2
      name: db-2

Convert the dictionaries into lists if you want to

  - set_fact:
      d2: "{{ d2|d({})|
              combine({item: dict(_keys|zip(_vals))|
                             dict2items(key_name='name', value_name='ip')}) }}"
    loop: "{{ groups.keys()|list|difference(['all', 'ungrouped']) }}"
    vars:
      _keys: "{{ groups[item] }}"
      _vals: "{{ groups[item]|
                 map('extract', hostvars, 'vhost_ip')|
                 list }}"
    run_once: true

gives

  d2:
    application:
      - ip: 10.0.2.1
        name: app-1
      - ip: 10.0.2.2
        name: app-2
    database:
      - ip: 10.0.1.1
        name: db-1
      - ip: 10.0.1.2
        name: db-2
    management:
      - ip: 10.0.0.1
        name: management-1

The usage is trivial

  roles:
    - management
      database_hosts: "{{ d2.database }}"

The dictionary d2 can be also used to create the lists if necessary

  database_ips: "{{ d2.database|map(attribute='ip')|list }}"
  application_ips: "{{ d2.application|map(attribute='ip')|list }}"

Note

You can keep the default groups all and ungrouped

          loop: "{{ groups.keys()|list }}"

You'll get

  d1:
    all:
      app-1: 10.0.2.1
      app-2: 10.0.2.2
      db-1: 10.0.1.1
      db-2: 10.0.1.2
      management-1: 10.0.0.1
    application:
      app-1: 10.0.2.1
      app-2: 10.0.2.2
    database:
      db-1: 10.0.1.1
      db-2: 10.0.1.2
    management:
      management-1: 10.0.0.1
    ungrouped: {}
  d2:
    all:
    - ip: 10.0.0.1
      name: management-1
    - ip: 10.0.1.1
      name: db-1
    - ip: 10.0.1.2
      name: db-2
    - ip: 10.0.2.1
      name: app-1
    - ip: 10.0.2.2
      name: app-2
    application:
    - ip: 10.0.2.1
      name: app-1
    - ip: 10.0.2.2
      name: app-2
    database:
    - ip: 10.0.1.1
      name: db-1
    - ip: 10.0.1.2
      name: db-2
    management:
    - ip: 10.0.0.1
      name: management-1
    ungrouped: []

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