Search code examples
ansibleansible-inventoryansible-facts

Use ansible filters to produce list of hosts based on conditions and defaults


I have written this a simplified example but it should be enough as a minimal reproducible code. I want to be able to generate a list of hosts from my inventory file based on 2 rules.

  1. The host belongs to a group called cdsre
  2. The host either has a attribute foo defined with a value of baz OR the host doesn't define an attribute foo

I have been at this for a few hours and can achieve this with a long winded jinja2 string loop that uses an if expression with a side effect which i think is pretty ugly. However I cant help think this should be achievable using just jinja filters.

sample inventory

all:
  children:
    cdsre:
      children:
        ovh_vm:
          hosts:
            ovh-vm[1:3]:
            ovh-vm[6:7]:
              foo: baz
        oracle_vm:
          hosts:
            oracle-vm[1:3]:
              foo: bar
            oracle-vm[4:5]:
              foo: baz

Playbook

---
- hosts: localhost
  gather_facts: false

  tasks:
    - set_fact:
        some_servers: |
          {% set servers = [] %}
          {% for host in groups['cdsre'] %}
          {% set foo = hostvars[host]['foo'] | default('baz', true) %}
          {% if foo == 'baz' %}
          {% if servers.append(hostvars[host]['inventory_hostname']) %}{% endif %}
          {% endif %}
          {% endfor %}
          {{ servers }}
        foo_matched_servers: "{{ groups['cdsre'] | map('extract', hostvars) | selectattr('foo', 'defined') | selectattr('foo', '==', 'baz') | map(attribute='inventory_hostname') | list}}"
    - debug:
        var: some_servers
    - debug:
        var: foo_matched_servers

OUTPUT

PLAY [localhost] ***********************************************************************************************************************************************************************************************************************

TASK [set_fact] ************************************************************************************************************************************************************************************************************************
Tuesday 10 January 2023  23:57:00 +0000 (0:00:00.073)       0:00:00.073 *******
ok: [localhost]

TASK [debug] ***************************************************************************************************************************************************************************************************************************
Tuesday 10 January 2023  23:57:01 +0000 (0:00:00.885)       0:00:00.958 *******
ok: [localhost] => {
    "some_servers": [
        "ovh-vm1",
        "ovh-vm2",
        "ovh-vm3",
        "ovh-vm6",
        "ovh-vm7",
        "oracle-vm4",
        "oracle-vm5"
    ]
}

TASK [debug] ***************************************************************************************************************************************************************************************************************************
Tuesday 10 January 2023  23:57:01 +0000 (0:00:00.061)       0:00:01.019 *******
ok: [localhost] => {
    "foo_matched_servers": [
        "ovh-vm6",
        "ovh-vm7",
        "oracle-vm4",
        "oracle-vm5"
    ]
}

PLAY RECAP *****************************************************************************************************************************************************************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Tuesday 10 January 2023  23:57:01 +0000 (0:00:00.062)       0:00:01.082 ******* 
===============================================================================
set_fact ---------------------------------------------------------------- 0.89s
debug ------------------------------------------------------------------- 0.12s
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
total ------------------------------------------------------------------- 1.01s

So i can produce the list of hosts matching condition 1 but What I cant seem to work out is how I can capture in a single fact the hosts that match condition 1 and condition 2. Is this even possible? or do i need to write an additional fact to capture the hosts matching condition 2 then join both facts (this also feels a bit long winded)


Solution

  • There are more options:

    1. Use Jinja to create a string with a valid YAML list and convert the string to the list

    Declare the variables below

      my_hosts_str: |
        [
        {% for host in groups.cdsre %}
        {% if hostvars[host]['foo']|default('baz') == 'baz' %}
        {{ host }},
        {% endif %}
        {% endfor %}
        ]
      my_hosts: "{{ my_hosts_str|from_yaml }}"
    

    gives the valid YAML list in the string my_hosts_str

      my_hosts_str: |-
        [
        ovh-vm1,
        ovh-vm2,
        ovh-vm3,
        ovh-vm6,
        ovh-vm7,
        oracle-vm4,
        oracle-vm5,
        ]
    

    gives the expected result. Converts the string to the list.

      my_hosts:
      - ovh-vm1
      - ovh-vm2
      - ovh-vm3
      - ovh-vm6
      - ovh-vm7
      - oracle-vm4
      - oracle-vm5
    

    Example of a playbook for testing

    - hosts: all
    
      vars:
    
        my_hosts_str: |
          [
          {% for host in groups.cdsre %}
          {% if hostvars[host]['foo']|default('baz') == 'baz' %}
          {{ host }},
          {% endif %}
          {% endfor %}
          ]
        my_hosts: "{{ my_hosts_str|from_yaml }}"
    
      tasks:
    
        - block:
            - debug:
                var: my_hosts_str
            - debug:
                var: my_hosts
          run_once: true
    

    1. Use filters to create the variable my_hosts.

    Declare the variables below, e.g. in group_vars/all

    shell> cat group_vars/all
    csdr_foo_und: "{{ groups.cdsre|map('extract', hostvars)|
                                   selectattr('foo', 'undefined')|
                                   map(attribute='inventory_hostname') }}"
    csdr_foo_baz: "{{ groups.cdsre|map('extract', hostvars)|
                                   selectattr('foo', 'defined')|
                                   selectattr('foo', '==', 'baz')|
                                   map(attribute='inventory_hostname') }}"
    my_hosts: "{{ csdr_foo_baz + csdr_foo_und }}"
    

    gives the list of hosts in the group csdr with undefined foo

      csdr_foo_und:
      - ovh-vm1
      - ovh-vm2
      - ovh-vm3
    

    gives the list of hosts in the group csdr with foo equal to baz

      csdr_foo_baz:
      - ovh-vm6
      - ovh-vm7
      - oracle-vm4
      - oracle-vm5
    

    gives the expected result. Concatenates the lists csdr_foo_und and csdr_foo_baz

      my_hosts:
      - oracle-vm4
      - oracle-vm5
      - ovh-vm1
      - ovh-vm2
      - ovh-vm3
      - ovh-vm6
      - ovh-vm7
    

    Example of a playbook for testing

    - hosts: all
    
      tasks:
    
        - block:
            - debug:
                var: csdr_foo_und
            - debug:
                var: csdr_foo_baz
            - debug:
                var: my_hosts|sort
          run_once: true
    

    1. Create a new group my_group

    Use the inventory plugin constructed. See

    shell> ansible-doc -t inventory ansible.builtin.constructed
    

    For example, create the project below for testing

    shell> tree .
    .
    ├── ansible.cfg
    ├── inventory
    │   ├── 01-hosts.yml
    │   └── 02-constructed.yml
    └── pb.yml
    
    1 directory, 4 files
    
    shell> cat ansible.cfg 
    [defaults]
    gathering = explicit
    inventory = $PWD/inventory
    retry_files_enabled = false
    stdout_callback = yaml
    
    shell> cat inventory/01-hosts.yml 
    all:
      children:
        cdsre:
          children:
            ovh_vm:
              hosts:
                ovh-vm[1:3]:
                ovh-vm[6:7]:
                  foo: baz
            oracle_vm:
              hosts:
                oracle-vm[1:3]:
                  foo: bar
                oracle-vm[4:5]:
                  foo: baz
    

    Create a new group my_group

    shell> cat inventory/02-constructed.yml 
    plugin: ansible.builtin.constructed
    use_vars_plugins: true
    use_extra_vars: true
    groups:
      # host belongs to group 'cdsre' and
      # foo is either undefined or 'baz'
      my_group: group_names is contains 'cdsre' and
                foo|default('baz') == 'baz'
    

    Either reference the group groups.my_group or use it in hosts

    shell> cat pb.yml 
    - hosts: all
      tasks:
        - debug:
            var: groups.my_group
          run_once: true
    
    - hosts: my_group
      tasks:
        - debug:
            var: ansible_play_hosts_all
          run_once: true
    

    gives

    shell> ansible-playbook pb.yml 
    
    PLAY [all] ***********************************************************************************
    
    TASK [debug] *********************************************************************************
    ok: [ovh-vm1] => 
      groups.my_group:
      - ovh-vm1
      - ovh-vm2
      - ovh-vm3
      - ovh-vm6
      - ovh-vm7
      - oracle-vm4
      - oracle-vm5
    
    PLAY [my_group] ******************************************************************************
    
    TASK [debug] *********************************************************************************
    ok: [ovh-vm1] => 
      ansible_play_hosts_all:
      - ovh-vm1
      - ovh-vm2
      - ovh-vm3
      - ovh-vm6
      - ovh-vm7
      - oracle-vm4
      - oracle-vm5
    
    PLAY RECAP ***********************************************************************************
    ovh-vm1: ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0