Search code examples
ansiblejinja2ansible-filter

Can an Ansible filter return Undefined?


I'm writing an Ansible filter. Is it possible that this filter returns Undefined, so that the resulting output is Undefined?

My goal is that I can use the filter in conjunction with default or select in situations like:

my_variable: "{{ my_input | myfilter | default(something) }}"

or:

my_list: "{{ list_of_things | map('myfilter') | select | list }}"

For example:

- vars:
    area: 9
    side: "{{ area | my_sqrt | default(0) }}"
  debug:
    var: side

or:

- vars:
    numbers: [16, 9, -4]
    roots: "{{ numbers | map('my_sqrt') | select | list }}"
  debug:
    var: roots

where map('my_sqrt') should output [4, 3, Undefined] and select should turn that into [4, 3].

I tried to defined my_sqrt as:

import math
from jinja2.runtime import Undefined

def my_sqrt(input):
    if input < 0:
        return Undefined
    else:
        return math.sqrt(input)

class FilterModule(object):
    def filters(self):
        return { 'my_sqrt': my_sqrt }

However, when I execute the above playbook, it seem to return the string <class 'jinja2.runtime.Undefined'>.

A bit of further testing proves this:

('never' if 0) | default('the_default') indeed returns the_default.

However, -4 | my_sqrt | default('the_default') sadly returns the string "<class 'jinja2.runtime.Undefined'>".

I've both tried with jinja2.runtime.Undefined and ansible.template.AnsibleUndefined.

I'm primarily interested to hear if an Ansible/Jinja2 filter can return Undefined.

If that's not possible, I'm interested to hear alternative approaches. What I'm actually am writing is a filter that takes an object and a list of options (possible matches), and will return the best option (best match). If there is no good option, it should return Undefined, after which it is up to the playbook to deal with the situation. As a basic requirement, it should work will with both default and select.


Solution

  • You could define your filter to return None instead of Undefined. Then you could pipe the output to both the select and default filter, with a caveat.

    None will be interpreted by the default filter as a boolean value. So, you must pass the value true as the second parameter of the default filter for it to work as expected.

    Try it using this code:

    Filter Definition:

    #!/bin/bash
    
    import math
    import typing as t
    
    V = t.TypeVar("V")
    
    def my_sqrt(value: V) -> t.Union[V , None]:
        if value < 0:
            return None
        else:
            return math.sqrt(value)
    
    class FilterModule(object):
        def filters(self):
            return {
                'my_sqrt': my_sqrt
            }
    

    Playbook example:

    - hosts: localhost
      gather_facts: no
      tasks:
        - name: Test the my_sqrt filter
          debug:
            msg: "{{ item | my_sqrt | default(0, true) }}"
          loop:
            - 4
            - -1
    
        - name: Select
          debug:
            msg: "{{ [4, 3, -4] | map('my_sqrt') | select | list }}"
    

    You'll see that both tasks work as expected:

    
    PLAY [localhost] ***************************************************************
    Friday 14 May 2021  09:27:41 -0300 (0:00:00.019)       0:00:00.019 ************ 
    
    TASK [Test the my_sqrt filter] *************************************************
    ok: [localhost] => (item=4) => 
      msg: '2.0'
    ok: [localhost] => (item=-1) => 
      msg: '0'
    Friday 14 May 2021  09:27:41 -0300 (0:00:00.042)       0:00:00.062 ************ 
    
    TASK [Select] ******************************************************************
    ok: [localhost] => 
      msg:
      - 2.0
      - 1.7320508075688772
    
    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    
    Friday 14 May 2021  09:27:41 -0300 (0:00:00.044)       0:00:00.106 ************ 
    =============================================================================== 
    Select ------------------------------------------------------------------ 0.04s
    Test the my_sqrt filter ------------------------------------------------- 0.04s