Search code examples
filteransibleyamlansible-role

Ansible role to filter host based on host variables


I am trying to create a ansible role which filter out host based on dictionary(passed to role) in comparison with host variables. If host variables contains key:value from dictionary it will added to filtered_host list. This is what I found till point but not getting desired output:

Ansible role:

---

- name: Filtering host
  debug:
    msg: "Here is the response {{ filters }} {{groups['all']}}"

- name: Setting filter
  set_fact:
    input_filter: "{{ filters }}"
  
- name: Setting filtered hosts
  set_fact:
    filtered_hosts: "{{ groups['all'] | map('extract', hostvars) | select('@ in filters') | map(attribute='inventory_hostname') | list }}"
  vars:
    input_filter: "{{ filters | to_json }}"

- name: Print filtered hosts
  debug:
    var: filtered_hosts

Here filters is a dictionary(filter) passed from playbook to test but actually i want to pass a dictionary, and this role should return list of hosts which contains key:value from this dictionary.

Sample dictionary:

{
   "os_type":"linux",
   "datacenter" : "REM",
   "location" : "IND"
}

Sample Host variables:

HOST A variables:

{
   "os_type":"linux",
   "datacenter" : "REM",
   "location" : "IND"
}

HOST B variables:

{
   "os_type":"linux",
   "datacenter" : "REM",
   "location" : "USA",
   "status" : "success"
}

HOST C variables:

{
   "os_type":"linux",
   "datacenter" : "REM",
   "location" : "IND",
   "status" : "success"
}

OUTPUT:

[Host A, Host C]

because both host contains key:value passed through dictionary. Also number of keys differ from different hosts.

Actual Scenario: I created a dynamic inventory which will fetch all host and host vars from DB based on some filters and assign it to inventory. Groups are created using some general info based on host vars. Now templates are created for different organizations/product who require to filter host based on some conditions(dynamically) out of the host vars. Since we cant create groups for everything, we decided to have a common ansible role which will act as a filter.

Within playbook there are multiple task will be performed on different host and to get those specific host some conditions are generated in the form of dictionary. These dictionary will be passed to ansible role to filter further based on the dynamic condition. The filter in given scenario is comparing 2 dictionaries to find out proper host for that task. A simplified example is what I have shown in the question.


Solution

  • Given the dictionary

      filters:
        os_type: linux
        datacenter: REM
        location: IND
    

    In the vars, select the keys

      fkeys: "{{ filters.keys() }}"
    

    Select and join the values

      fvals: "{{ filters.values()|join(',') }}"
    

    gives

      fvals: linux,REM,IND
    

    In the tasks, create the variable my_fvals

        - set_fact:
            my_fvals: "{{ lookup('vars', *fkeys, default='UNDEF') }}"
        - debug:
            var: my_fvals
    

    gives (abridged)

    ok: [Host_A] => 
      my_fvals: linux,REM,IND
    ok: [Host_B] => 
      my_fvals: linux,REM,USA
    ok: [Host_C] => 
      my_fvals: linux,REM,IND
    

    Select hosts that match the criteria

      filtered_hosts: "{{ hostvars|dict2items|
                          selectattr('value.my_fvals', '==', fvals)|
                          map(attribute='key') }}"
    

    gives

      filtered_hosts: [Host_A, Host_C]
    

    • Example of a complete playbook for testing
    - hosts: all
    
      vars:
    
        filters:
          os_type: linux
          datacenter: REM
          location: IND
    
        fkeys: "{{ filters.keys() }}"
        fvals: "{{ filters.values()|join(',') }}"
        filtered_hosts: "{{ hostvars|dict2items|
                            selectattr('value.my_fvals', '==', fvals)|
                            map(attribute='key') }}"
    
      tasks:
    
        - debug:
            var: fvals
          run_once: true
    
        - set_fact:
            my_fvals: "{{ lookup('vars', *fkeys, default='UNDEF') }}"
        - debug:
            var: my_fvals
    
        - debug:
            var: filtered_hosts|to_yaml
          run_once: true
    
    • You can create a role if you want to
    shell> tree roles
    roles/
    └── filtered_hosts
        ├── defaults
        │   └── main.yml
        └── tasks
            └── main.yml
    
    3 directories, 2 files
    
    shell> cat roles/filtered_hosts/defaults/main.yml 
    fkeys: "{{ filters.keys() }}"
    fvals: "{{ filters.values()|join(',') }}"
    
    shell> cat roles/filtered_hosts/tasks/main.yml 
    - set_fact:
        my_fvals: "{{ lookup('vars', *fkeys, default='UNDEF') }}"
    - set_fact:
        filtered_hosts: "{{ hostvars|dict2items|
                            selectattr('value.my_fvals', '==', fvals)|
                            map(attribute='key') }}"
      run_once: true
    

    The playbook

    shell> cat pb.yml
    - hosts: all
    
      vars:
    
        filters:
          os_type: linux
          datacenter: REM
          location: IND
    
      roles:
        - filtered_hosts
    
      tasks:
    
        - debug:
            var: filtered_hosts|to_yaml
          run_once: true
    

    gives

    shell> ansible-playbook pb.yml
    
    PLAY [all] ***********************************************************************************
    
    TASK [filtered_hosts : set_fact] *************************************************************
    ok: [Host_A]
    ok: [Host_C]
    ok: [Host_B]
    
    TASK [filtered_hosts : set_fact] *************************************************************
    ok: [Host_A]
    
    TASK [debug] *********************************************************************************
    ok: [Host_A] => 
      filtered_hosts|to_yaml: |-
        [Host_A, Host_C]
    
    PLAY RECAP ***********************************************************************************
    Host_A: ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    Host_B: ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    Host_C: ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    
    • It is not possible to use the inventory plugin ansible.builtin.constructed here
    shell> tree inventory
    inventory/
    ├── 01-hosts
    └── 02-constructed.yml
    
    0 directories, 2 files
    
    shell> cat inventory/01-hosts 
    Host_A
    Host_B
    Host_C
    
    shell> cat inventory/02-constructed.yml 
    plugin: ansible.builtin.constructed
    use_extra_vars: true
    use_vars_plugins: true
    strict: true
    compose:
      fkeys: filters.keys()
      fvals: filters.values()|join(',')
      my_fvals: lookup('vars', *filters.keys(), default='UNDEF')
    groups:
      filtered_hosts: my_fvals == fvals
    

    because in this inventory plugin lookups were disabled from templating

    shell> ansible-inventory -i inventory -e @filters.yml --list --yaml
    

    [WARNING]: * Failed to parse /export/scratch/tmp7/test-477/inventory/02-constructed.yml with auto plugin: failed to parse /export/scratch/tmp7/test-477/inventory/02-constructed.yml: Could not set my_fvals for host Host_A: The lookup vars was found, however lookups were disabled from templating . Could not set my_fvals for host Host_A: The lookup vars was found, however lookups were disabled from templating

    [WARNING]: * Failed to parse /export/scratch/tmp7/test-477/inventory/02-constructed.yml with yaml plugin: Plugin configuration YAML file, not YAML inventory

    [WARNING]: * Failed to parse /export/scratch/tmp7/test-477/inventory/02-constructed.yml with ini plugin: Invalid host pattern 'plugin:' supplied, ending in ':' is not allowed, this character is reserved to provide a port.

    [WARNING]: Unable to parse /export/scratch/tmp7/test-477/inventory/02-constructed.yml as an inventory source

    all:
      children:
        ungrouped:
          hosts:
            Host_A:
              datacenter: REM
              filters: &id001
                datacenter: REM
                location: IND
                os_type: linux
              fkeys:
              - os_type
              - datacenter
              - location
              fvals: linux,REM,IND
              location: IND
              os_type: linux
            Host_B:
              datacenter: REM
              filters: *id001
              location: USA
              os_type: linux
              status: success
            Host_C:
              datacenter: REM
              filters: *id001
              location: IND
              os_type: linux
              status: success