Search code examples
ansibleansible-facts

In Ansible, how to query hostvars to get a specific value of a key from a list item based on the value of a different key?


EDIT-UPDATE:

I found a way to achieve what was trying to do, using the index_of plugin. The following code outputs what I need.

---
- hosts: CASPOSR1BDAT003
  connection: local
  gather_facts: no
  become: false
  tasks:
    - ansible.builtin.set_fact:
        mac_address: "{{ hostvars[inventory_hostname]['interfaces'][int_idx|int]['mac_address'] }}"
      vars:
        int_name: 'PCI1.1'
        int_idx: "{{ lookup('ansible.utils.index_of', hostvars[inventory_hostname]['interfaces'], 'eq', int_name, 'name') }}"
    - debug:
        var: mac_address

Output:

PLAY [CASPOSR1BDAT003] ***********************************************************************************************************************************************************************************************

TASK [ansible.builtin.set_fact] **************************************************************************************************************************************************************************************
ok: [CASPOSR1BDAT003]

TASK [debug] *********************************************************************************************************************************************************************************************************
ok: [CASPOSR1BDAT003] => 
  mac_address: 20:67:7C:00:36:A0

What I am trying to do:

  • Use the Netbox dynamic inventory plugin (this works, brings back all the info I need)
  • Query hostvars for a particular host, and get the value of the MAC address for a particular interface called PCI1.1

What I have tried:

  1. Converting the hostvars to JSON and using json_query: this hasn't worked, and having looked at some issues on GitHub, hostvars isn't a "normal" dictionary. I've logged a couple of issues anyway (https://github.com/ansible/ansible/issues/76289 and https://github.com/ansible-collections/community.general/issues/3706).
  2. Use a sequence loop and conditional "when" to get the value - this sort of works when using the debug module, but still not just returning the value

What works: I have tried the following, which outputs the mac_address variable as expected. The length of the list is found, and then the conditional matches the name. I do get an warning about using jinja2 templating delimiters but that's not the target of this question.

---
- hosts: CASPOSR1BDAT003
  connection: local
  gather_facts: no
  become: false
  tasks:
    - debug:
        var: hostvars[inventory_hostname]['interfaces'][{{ item }}]['mac_address']
      with_sequence: start=0 end="{{ end_at }}"
      vars:
        - end_at: "{{ (hostvars[inventory_hostname]['interfaces'] | length) - 1 }}"
      when: hostvars[inventory_hostname]['interfaces'][{{ item }}]['name'] == "PCI1.1"

The result is:

TASK [debug] *************************************************************************************************************************************
[WARNING]: conditional statements should not include jinja2 templating delimiters such as {{ }} or {% %}. Found:
hostvars[inventory_hostname]['interfaces'][{{ item }}]['name'] == "PCI1.1"
skipping: [CASPOSR1BDAT003] => (item=0) 
skipping: [CASPOSR1BDAT003] => (item=1) 
skipping: [CASPOSR1BDAT003] => (item=2) 
skipping: [CASPOSR1BDAT003] => (item=3) 
skipping: [CASPOSR1BDAT003] => (item=4) 
ok: [CASPOSR1BDAT003] => (item=5) => 
  ansible_loop_var: item
  hostvars[inventory_hostname]['interfaces'][5]['mac_address']: 20:67:7C:00:36:A0
  item: '5'
skipping: [CASPOSR1BDAT003] => (item=6) 
skipping: [CASPOSR1BDAT003] => (item=7) 
skipping: [CASPOSR1BDAT003] => (item=8) 
skipping: [CASPOSR1BDAT003] => (item=9) 

I'm trying to use set_fact to store this mac_address variable as I need to use it in a couple of different ways. However, I am unable to use set_fact on this (or any other hostvars data, it seems). For example, the following:

---
- hosts: CASPOSR1BDAT003
  connection: local
  gather_facts: no
  become: false
  tasks:
    - ansible.builtin.set_fact:
        interfaces: "{{ hostvars[inventory_hostname]['interfaces'][item]['mac_address'] }}"
      with_sequence: start=0 end="{{ end_at }}"
      vars:
        - end_at: "{{ (hostvars[inventory_hostname]['interfaces'] | length) - 1 }}"
      when: hostvars[inventory_hostname]['interfaces'][{{ item }}]['name'] == "PCI1.1"
    - debug:
        var: interfaces

results in:

fatal: [CASPOSR1BDAT003]: FAILED! => 
  msg: |-
    The task includes an option with an undefined variable. The error was: 'list object' has no attribute '5'
  
    The error appears to be in '/Users/kivlint/Documents/GitHub/vmware-automation/ansible/prepare-pxe.yml': line 19, column 7, but may
    be elsewhere in the file depending on the exact syntax problem.
  
    The offending line appears to be:
  
        #   when: hostvars[inventory_hostname]['interfaces'][{{ item }}]['name'] == "PCI1.1"
        - ansible.builtin.set_fact:
          ^ here

If I hard-code the number 5 in, it works fine:

TASK [ansible.builtin.set_fact] ******************************************************************************************************************
ok: [CASPOSR1BDAT003]

TASK [debug] *************************************************************************************************************************************
ok: [CASPOSR1BDAT003] => 
  interfaces: 20:67:7C:00:36:A0

If I use '5' as a var for the task, it also works.

---
- hosts: CASPOSR1BDAT003
  connection: local
  gather_facts: no
  become: false
  tasks:
    - ansible.builtin.set_fact:
        interfaces: "{{ hostvars[inventory_hostname]['interfaces'][int_index]['mac_address'] }}"
      vars:
        - int_index: 5

So I'm wondering, is this a "bug/feature" in how set_fact does or doesn't work with loops (meaning, the same loop worked fine with debug? Or do I need to re-think the approach and consider trying to use set_fact to set a variable with the index of the list (e.g. 5 in the above example)? Or something else?


Solution

  • There's a lot going on in your code, and achieving the result you want is simpler than you've made it.

    Firstly, don't use hostvars[inventory_hostname]; plain variables are the ones belonging to the current host, and going through hostvars introduces some exciting opportunities for things to go wrong. hostvars is for accessing variables belonging to other hosts.

    Secondly, using Jinja's built-in filtering capabilities avoids the need to worry about the index of the item that you want.

    - hosts: CASPOSR1BDAT003
      connection: local
      gather_facts: no
      become: false
      vars:
        int_name: PCI1.1
        mac_address: "{{ interfaces | selectattr('name', 'eq', int_name) | map(attribute='mac_address') | first }}"
      tasks:
        - debug:
            var: mac_address