Search code examples
ansiblejinja2ansible-inventoryansible-templatejson-query

How to match/search a substring from a dict attribute that is a list


Here's the scenario:

  • a playbook that calls a role to create users in multiple servers, including a VM Scale Set (where ansible_hostnames can't be predicted) - inventory is already being dynamically generated and works fine and not the issue
  • a users dict variable will provide the user list as well as a series of attributes for each
  • one of these attributes is a server list named target_servers - this variable's attribute is the actual issue
  • target_servers is used by the playbook to decide if the user will be present/absent on that particular server - it complements ansible's inventory
  • target_servers might include only the starting name of a particular target host, a sub-string, like "vmss" as a "vmss*" wildcard, but also fixed hostnames server12345, server12346, etc.
  • so, dynamic inventory tells ansible which servers to connect to, but the variable tells it whether the user should be created or removed from that particular servers (i.e. servers have different users)

Objective(s):

Have a conditional that checks if a target_server list element content matches the ansible_hostname (i.e. if the substring found in the target_servers list (from the users dict) matches, then we provision the user; additionally, off course, if the list provides the entire hostname, it should match and the users also be provisioned)

Here's the code:

---
- hosts: all
  become: yes
  vars:
    users:
      user1:
          is_sudo: no
          is_chrooted: yes
          auth_method: hvault
          sa_homedir: firstname1lastname1
          state: present
          target_servers:
            - vmss
            - ubuntu
      user2:
          is_sudo: no
          is_chrooted: yes
          auth_method: hvault
          sa_homedir: firstname2lastname2
          state: present
          target_servers:
            - vmss
            - ubuntu18
  tasks:
  - debug:
      msg: "{{ ansible_hostname }}"

  - debug:
      msg: "{{ item.value.target_servers }}"
    loop: "{{ lookup('dict', users|default({})) }}"

  # This is just to exemplify what I'm trying to achieve as it is not supposed to work
  - debug:
      msg: "ansible_hostname is in target_servers of {{ item.key }}"
    loop: "{{ lookup('dict', users|default({})) }}"
    when: ansible_hostname is match(item.value.target_servers)

Here's the output showing that the match string test cannot be applied to a list (as expected):

TASK [debug] ************************************************************************************************************************************************
ok: [ubuntu18] =>
  msg: ubuntu18

TASK [debug] ************************************************************************************************************************************************
ok: [ubuntu18] => (item={'key': 'user1', 'value': {'is_sudo': False, 'is_chrooted': True, 'auth_method': 'hvault', 'sa_homedir': 'firstname1lastname1', 'state': 'present', 'target_servers': ['vmss', 'ubuntu']}}) =>
  msg:
  - vmss
  - ubuntu
ok: [ubuntu18] => (item={'key': 'user2', 'value': {'is_sudo': False, 'is_chrooted': True, 'auth_method': 'hvault', 'sa_homedir': 'firstname2lastname2', 'state': 'present', 'target_servers': ['vmss', 'ubuntu18']}}) =>
  msg:
  - vmss
  - ubuntu18

TASK [debug] ************************************************************************************************************************************************
fatal: [ubuntu18]: FAILED! =>
  msg: |-
    The conditional check 'ansible_hostname is match(item.value.target_servers)' failed. The error was: Unexpected templating type error occurred on ({% if ansible_hostname is match(item.value.target_servers) %} True {% else %} False {% endif %}): unhashable type: 'list'

    
    The error appears to be in 'test-play-users-core.yml': line 32, column 5, but may
    be elsewhere in the file depending on the exact syntax problem.

    The offending line appears to be:


      - debug:
        ^ here

Already tried researching about selectattr, json_query and subelements but I currently lack the understanding on how to make them work to match a substring inside a dict attribute that is a list.

In the example above, by changing from is match() to in, exact hostnames work fine, but that is not the goal. I need to match both exact hostnames and sub-strings for these hostnames.

Any help on how to accomplish this or suggestions about alternate methods will be greatly appreciated.


The example here might work if I could find a way to run it against a list (target_servers) after having already looped through the entire dictionary (are nested loops possible?): https://docs.ansible.com/ansible/latest/user_guide/playbooks_tests.html#testing-strings

I guess I've just found what I needed: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/subelements_lookup.html
Will try and provide an update soon.

Update: yes, subelements work! Here's the code needed:

- name: test 1
  debug:
    msg: "{{ item.1 }} matches {{ ansible_hostname }}"
  with_subelements:
    - "{{ users }}"
    - target_servers
  when: >
    ansible_hostname is match(item.1)

Solution

  • You can use the select filter to apply the in test to all the elements of your users' target_servers list.

    This would be your debug task:

    - debug:
        msg: "hostname is in target_servers of {{ item.key }}"
      loop: "{{ users | dict2items  }}"
      loop_control:
        label: "{{ item.key }}"
      when: >-
        item.value.target_servers 
        | select('in', inventory_hostname) 
        | length > 0
    

    Given the playbook:

    - hosts: all
      gather_facts: false
      vars:
        _hostname: ubuntu18
        users:
          user1:
              target_servers:
                - vmss
                - ubuntu
          user2:
              target_servers:
                - vmss
                - ubuntu18
    
      tasks:
        - debug:
            msg: "hostname is in target_servers of {{ item.key }}"
          loop: "{{ users | dict2items  }}"
          loop_control:
            label: "{{ item.key }}"
          when: >-
            item.value.target_servers 
            | select('in', inventory_hostname) 
            | length > 0
    

    This yields:

    ok: [ubuntu18] => (item=user1) => 
      msg: hostname is in target_servers of user1
    ok: [ubuntu18] => (item=user2) => 
      msg: hostname is in target_servers of user2
    

    Doing it with subelements instead:

    - hosts: all
      gather_facts: false
      vars:
        _hostname: ubuntu18
        users:
          user1:
              target_servers:
                - vmss
                - ubuntu
          user2:
              target_servers:
                - vmss
                - ubuntu18
    
      tasks:
        - debug:
            msg: "hostname is in target_servers of {{ item.0.key }}"
          loop: "{{ users | dict2items | subelements('value.target_servers')  }}"
          loop_control:
            label: "{{ item.0.key }}"
          when: item.1 in inventory_hostname
    

    Will yield:

    skipping: [ubuntu18] => (item=user1) 
    ok: [ubuntu18] => (item=user1) => 
      msg: hostname is in target_servers of user1
    skipping: [ubuntu18] => (item=user2) 
    ok: [ubuntu18] => (item=user2) => 
      msg: hostname is in target_servers of user2