TommyKTheDJ
TommyKTheDJ

Reputation: 23

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:

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?

Upvotes: 2

Views: 6266

Answers (3)

flowerysong
flowerysong

Reputation: 2969

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

Upvotes: 1

Frenchy
Frenchy

Reputation: 16997

there is a confusion between the [5] (6th item of a list) and ['5'] (a key named "5") , you see in your error: The error was: 'list object' has no attribute '5'.

with the module debug you have not error because [{{item}}] is replaced by [5] and not by ['5']. Its not the same thing with set_fact.

its the reason you have to use filter int to clarify the situation.

  - ansible.builtin.set_fact:
        interfaces: "{{ hostvars[inventory_hostname]['interfaces'][item|int]['mac_address'] }}"
      with_sequence: start=0 end="{{ end_at }}"
      vars:
        end_at: "{{ (hostvars[inventory_hostname]['interfaces'] | length) - 1 }}"
      when: hostvars[inventory_hostname]['interfaces'][item|int]['name'] == "PCI1.1"

so i suggest you to use loop instead with_sequence:

- ansible.builtin.set_fact:
    interfaces: "{{ hostvars[inventory_hostname]['interfaces'][item]['mac_address'] }}"
  loop: "{{ range(0, end_at|int, 1)|list }}"
  vars:
    end_at: "{{ hostvars[inventory_hostname]['interfaces'] | length }}"
  when: hostvars[inventory_hostname]['interfaces'][item]['name'] == "PCI1.1"

Upvotes: 1

George Shuklin
George Shuklin

Reputation: 7867

set_fact works with loops, but not in a way you expect.

This example constructs list with loop from lists of dicts:

- set_fact:
    foo: '{{ foo|d([]) + [item.value] }}'
  loop:
    - value: 1
    - value: 2

Basically, each execution of set_fact creates a fact. You may refer to the same fact in jinja expression for set_fact, but you can't expect it to automatically build lists or something like that.

Upvotes: 0

Related Questions