1

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)
β.εηοιτ.βε
  • 33,893
  • 13
  • 69
  • 83
rofz
  • 95
  • 8
  • Try `when: ansible_hostname in item.value.target_servers`. See [doc](https://jinja.palletsprojects.com/en/latest/templates/#jinja-tests.in). – Vladimir Botka Mar 28 '22 at 15:26
  • Thanks @VladimirBotka, but as I stated in the paragraph '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.' this does not address the issue. – rofz Mar 28 '22 at 15:55
  • Have you tested it? As Python consider strings as a list of characters, doing `'bar' in 'foobarbaz'` would give you a `true`. – β.εηοιτ.βε Mar 28 '22 at 17:26
  • Thanks @β.εηοιτ.βε, I'm looking to match target_servers (where we can have a sub-string) against ansible_hostname, so the logic is the opposite of what Vladimir suggested. Plus, the attribute target_servers is a list of strings, not a single string. 'when: item.value.target_servers in ansible_hostname' leads me to the following self-explanatory error: "Unexpected templating type error occurred on ({% if item.value.target_servers in ansible_hostname %} True {% else %} False {% endif %}): 'in ' requires string as left operand, not list". But I might be mistaken and missing your point. – rofz Mar 28 '22 at 19:14

1 Answers1

1

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
β.εηοιτ.βε
  • 33,893
  • 13
  • 69
  • 83
  • Thanks @Benoit. Altough I had found an answer using subelements, it led me to change my main variable (users) and add an additional attribute named username (instead of item.key - I could not find a proper way to refer to item.0.key with subelements). Your answer apparently does not require me to change such variable, so it looks much better. Thank you. Marking it as answer. – rofz Mar 28 '22 at 20:14
  • That can also be managed with `subelements` ([prefer the filter over the soon-to-be-deprecated `with_*`](https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#migrating-from-with-x-to-loop)), you will just get more _skipped_ in your loop (see updated edit). – β.εηοιτ.βε Mar 28 '22 at 20:27