Search code examples
ansiblejinja2jmespath

Create list of dict filtering different value with Ansible


I'm looking for a better solution to filter a list of dicts by a key A and return a list of value of key B. To be more concrete - every host has a dict:

infrastructure:
  name: "x..."
network:
  address: "1..."

There are hosts, where network.address is defined and there are hosts, where network.address is not defined. I need now a list of all infrastructure.name with defined network.address.

- name: "Define Alias fact"
  set_fact:
    alias: []

- name: "Add Aliases for all hosts with network.address is defined"
  set_fact:
    alias: "{{ alias + [hostvars[host].infrastructure.name + '-alias'] }}"
  when:
    - "hostvars[host].network is defined"
    - "hostvars[host].network.address is defined"
  with_items: "{{ groups['all'] }}"
  loop_control:
    loop_var: host

That works, but is a little bit messy, because I call set_fact many times and add items to a list.

When I have a look at:

- name: "Define addresses fact"
  set_fact:
    address: "{{ groups['all'] | map('extract', hostvars) | list | json_query('[*].network.address') }}"

This is much shorter, maybe easier.

I'd like to ask, if I can use map and extract and the "list of dicts" before flatten the list to "filter out" all items where network.address is not defined and use json_query together with some string operation to append the '-alias'. Is there a similar easy way to replace the first script?


Solution

  • In a pure JMESPath way, given the JSON

    [
      {
        "infrastructure": {"name": "x..."},
        "network": {"address": "1..."}
      },
      {
        "infrastructure": {"name": "y..."}
      },
      {
        "infrastructure": {"name": "z..."},
        "network": {"address": "2..."}
      },
      {
        "infrastructure": {"name": "a..."},
        "network": {}
      }
    ]
    

    You can extract the infrastructure.name concatenated with -alias having a network.address set this way:

    [?network.address].join('-', [infrastructure.name, 'alias'])
    

    This will yield:

    [
      "x...-alias",
      "z...-alias"
    ]
    

    The function join is primarily meant to glue array elements together, but it can also be used to concatenate string.


    And so for a playbook demonstrating this:

    - hosts: all
      gather_facts: no
          
      tasks:
        - debug: 
            msg: >-
              {{ 
                servers | to_json | from_json | 
                json_query(
                  '[?network.address].join(`-`, [infrastructure.name, `alias`])'
                ) 
              }}
          vars:
            servers:
              - infrastructure:
                  name: x...
                network:
                  address: 1...
              - infrastructure:
                  name: y...
              - infrastructure:
                  name: z...
                network:
                  address: 2...
              - infrastructure:
                  name: a...
                network:
    

    Note that the odd construct | from_json | to_json is explained in this other answer

    This yields:

    PLAY [all] ********************************************************************************************************
    
    TASK [debug] ******************************************************************************************************
    ok: [localhost] => {
        "msg": [
            "x...-alias",
            "z...-alias"
        ]
    }
    
    PLAY RECAP ********************************************************************************************************
    localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0