Home > Enterprise >  How to combine/match list with specific key/values as hostvar
How to combine/match list with specific key/values as hostvar

Time:08-25

I have a question about dicts and lists.

What I want to achieve is, have the key and value from a seperate list saved as fact/hostvar for each matching host.

I'm getting a list from a Confluence API that looks like this (abbreviated):

[
    {
        "title": "MACHINE1",
        "_links": {
            "tinyui": "/x/1234"
        }
    },
    {
        "title": "MACHINE2",
        "_links": {
            "tinyui": "/x/5678"
        }
    },
    {
        "title": "MACHINE3",
        "_links": {
            "tinyui": "/x/9876"
        }
    }
]

What worked to get each individual item (just to debug, and show that the loop itself works) is:

- name: DEBUG specific item in list of get_children.json.results
  debug:
    msg: "{{ item.title }} {{ item._links.tinyui }}"
  loop:
    "{{ get_children.json.results }}"
  delegate_to: 127.0.0.1

Ansible Output (here: output for only one machine):

"msg": "MACHINE1 /x/1234"

Machine Hostnames:

Yes, they are lowercase in my inventory, and in the above list output they are uppercase. But I guess a simple item.title|lower would do fine.

machine1
machine2
machine3

How can I now match the item.title with ansible_hostname and save above API Output as a fact for each machine?

And for clarification: item.title|lower == ansible_hostname

I hope it gets clear to what I want to achieve and thanks to everyone in advance :)

EDIT: Thanks to both answers I managed to get it to work. Using '(?i)^' VAR '$' and some other conditional checks you guys posted definitely helped. :)

CodePudding user response:

In a nutshell, given the inventories/tinyui/main.yml inventory:

---
all:
  hosts:
    machine1:
    machine2:
    machine3:
    i.do.not.exist:

The folowing tinyui.yml playbook:

---
- hosts: all
  gather_facts: false

  vars:
    # In real life, this is returned by your API call
    get_children:
      json:
        results: [
          {
            "title": "MACHINE1",
            "_links": {
              "tinyui": "/x/1234"
            }
          },
          {
            "title": "MACHINE2",
            "_links": {
              "tinyui": "/x/5678"
            }
          },
          {
            "title": "MACHINE3",
            "_links": {
              "tinyui": "/x/9876"
            }
          }
        ]
    
    # This won't be defined before you call the API which
    # returns and registers the correct result. If there is
    # no match for host in the returned json, '!no uri!' will
    # be returned below. Adapt with a default uri if needed
    tinyui: "{{ get_children.json.results 
      | selectattr('title', '==', inventory_hostname | upper)
      | map(attribute='_links.tinyui') 
      | default(['!no uri!'], true) | first }}"

  tasks:
    # In real life, you would have called your API
    # and registered the result in `get_children` e.g.
    # - name: get info from confluence
    #   uri:
    #     url: <confluence api endpoint url>
    #     <more parameters here>
    #   run_once: true
    #   delegate_to: localhost
    #   register: get_children
 
    - name: Display tinyui for host
      debug:
        msg: "tinyui for host {{ inventory_hostname }} is {{ tinyui }}"

Gives:

$ ansible-playbook -i inventories/tinyui/ tinyui.yml 

PLAY [all] ***********************************************************************************************************************

TASK [Display tinyui for host] ***************************************************************************************************
ok: [machine1] => {
    "msg": "tinyui for host machine1 is /x/1234"
}
ok: [machine2] => {
    "msg": "tinyui for host machine2 is /x/5678"
}
ok: [machine3] => {
    "msg": "tinyui for host machine3 is /x/9876"
}
ok: [i.do.not.exist] => {
    "msg": "tinyui for host i.do.not.exist is !no uri!"
}


PLAY RECAP ***********************************************************************************************************************
i.do.not.exist             : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
machine1                   : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
machine2                   : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
machine3                   : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

CodePudding user response:

You can filter the list of dictionaries by title, taking out the right dict.

You can do this with the following line:

"{{ hostsdata | selectattr('title', 'match', '(?i)^' host '$') | first }}"
  • With selectattr you filter your list of dicts by the title, where this must match '(?i)^' host '$'.
    The (?i) is the ignore case inline flag for pattern matching, concatenated with the hostname (case does not matter because of ignore case flag). ^...$ specifies that the whole string must match, from start to end.
  • Since selectattr returns a list as result, you can use first to take out the first element of the list.

Instead of using (?i) you can also set the ignorecase parameter, which will look like this:

"{{ hostsdata | selectattr('title', 'match', '^' host '$', 'ignorecase=true') | first }}"

Both variants work equivalently.


Entire playbook:

---
- hosts: localhost
  gather_facts: no

  vars:
    hostsdata:
      - {
            "title": "MACHINE1",
            "_links": {
                "tinyui": "/x/1234"
            }
        }
      - {
            "title": "MACHINE2",
            "_links": {
                "tinyui": "/x/5678"
            }
        }
      - {
            "title": "MACHINE3",
            "_links": {
                "tinyui": "/x/9876"
            }
        }

  tasks:
    - debug:
        var: hostsdata

    - name: Pick out host specific dict
      set_fact:
        machine_data: "{{ hostsdata | selectattr('title', 'match', '(?i)' host) | first }}"
      vars:
        host: machine3

    - debug:
        var: machine_data

    - debug:
        msg: "{{ machine_data.title }} {{ machine_data._links.tinyui }}"

Resulting output:

TASK [debug] ***********************************************************************************************************
ok: [localhost] => {
    "hostsdata": [
        {
            "_links": {
                "tinyui": "/x/1234"
            },
            "title": "MACHINE1"
        },
        {
            "_links": {
                "tinyui": "/x/5678"
            },
            "title": "MACHINE2"
        },
        {
            "_links": {
                "tinyui": "/x/9876"
            },
            "title": "MACHINE3"
        }
    ]
}

TASK [Pick out host specific dict] *************************************************************************************
ok: [localhost]

TASK [debug] ***********************************************************************************************************
ok: [localhost] => {
    "machine_data": {
        "_links": {
            "tinyui": "/x/9876"
        },
        "title": "MACHINE3"
    }
}

TASK [debug] ***********************************************************************************************************
ok: [localhost] => {
    "msg": "MACHINE3 /x/9876"
}

To filter multiple machines, here is another example:

- debug:
    msg: "{{ md.title }} {{ md._links.tinyui }}"
  when: md | length
  vars:
    md: "{{ hostsdata | selectattr('title', 'match', '(?i)^' item '$') | first | default('') }}"
  with_items:
    - MachINE1
    - MACHINE2
    - machine3
    - unknown

Add a default('') and a when: to skip a non-existent hostname.

Output:

TASK [debug] ***********************************************************************************************************
ok: [localhost] => (item=MachINE1) => {
    "msg": "MACHINE1 /x/1234"
}
ok: [localhost] => (item=MACHINE2) => {
    "msg": "MACHINE2 /x/5678"
}
ok: [localhost] => (item=machine3) => {
    "msg": "MACHINE3 /x/9876"
}
skipping: [localhost] => (item=unknown)
  • Related