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
.
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