In my Ansible roles, some roles derive specific configuration settings from global variables Global variables might be undefined. The following code illustrates the schema:
- hosts: localhost
vars:
bar: '{{ foo }}'
tasks:
# Assume foo comes from an Ansible environment
- debug: var=foo
# Assume bar comes from a role default
- debug: var=bar
# Catched by the "is defined" condition
- debug: msg="foo is defined"
when: 'foo is defined'
# Cannot catch undefined exception?!
- debug: msg="bar is defined"
when: 'bar is defined'
Everything works as expected, but the last statement: Ansible raises an exception because foo is undefined (yes, it is undefined).
PLAY [localhost] *********************************************************************************************************************************************************
TASK [Gathering Facts] ***************************************************************************************************************************************************
ok: [localhost]
TASK [debug] *************************************************************************************************************************************************************
ok: [localhost] => {
"foo": "VARIABLE IS NOT DEFINED!"
}
TASK [debug] *************************************************************************************************************************************************************
ok: [localhost] => {
"bar": "VARIABLE IS NOT DEFINED!"
}
TASK [debug] *************************************************************************************************************************************************************
skipping: [localhost]
TASK [debug] *************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The conditional check 'bar is defined' failed. The error was: error while evaluating conditional (bar is defined): {{ foo }}: 'foo' is undefined\n\nThe error appears to be in '.../test-undef.yml': line 9, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n when: 'foo is defined'\n - debug: msg=\"bar is defined\"\n ^ here\n"}
So why does bar not “evaluate” to undefined like foo? And how can I trap this “multi-level” undefinedness?
Try this:
- hosts: localhost
vars:
bar: '{{ foo }}'
tasks:
# Assume foo comes from an Ansible environment
- debug: var=vars.foo
# Assume bar comes from a role default
- debug: var=vars.bar
# Catched by the "is defined" condition
- debug: msg="foo is defined"
when: vars.foo is defined
# Cannot catch undefined exception?!
- debug: msg="bar is defined"
when: vars.bar is defined
The problem is that bar is technically defined, and your definition of bar uses a possibly undefined variable. When you attempt to do anything with bar it has to be evaluated as an independent Jinja expression, which happens before the is defined check.
One way to address this is to make it so bar can be evaluated without resulting in an undefined value, e.g.
- hosts: localhost
vars:
bar: "{{ foo | default(false) }}"
tasks:
- debug:
msg: bar is truthy
when: bar is truthy
You can also check foo before bar since evaluation is short-circuitable, but baking knowledge of the variable relationship into your tasks can be unwieldy.
- hosts: localhost
vars:
bar: "{{ foo }}"
tasks:
- debug:
msg: bar is truthy
when:
- foo is defined
- bar is defined
try adding
when: ( vars[bar] is defined )
Related
I need to modify a playbook that expects to have a yaml file loaded via --extra-vars because the extra vars file needs to be generated with user input every time it's used.
This playbook currently overwrites a file and then modifies it, replacing placeholders with the values in my-extra-vars.yml. This is a destructive step; if a variable is not defined then the placeholder is left untouched.
This is working, sort of, but the error message is not what I expect or want.
vars:
- required_vars:
- 'varname1'
- 'varname2'
tasks:
- name: Check for required variables
fail:
msg: "variable '{{item}}' is not defined"
when: lookup('vars', item) is undefined
with_items: "{{required_vars}}"
Running the playbook as ansible playbook -K --extra-vars '#test-vars.yml' test.yml with only varname1 defined gives me the following output.
TASK [Check for required variables] **************************************************************************************
skipping: [127.0.0.1] => (item=varname1)
fatal: [127.0.0.1]: FAILED! => {"msg": "The conditional check 'lookup('vars', item) is undefined' failed. The error was: error while evaluating conditional (lookup('vars', item) is undefined): No variable found with this name: varname2\n\nThe error appears to be in '/home/me/projects/test/test.yml': line 21, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n tasks:\n - name: Check for required variables\n ^ here\n"}
If all variables are defined, then there is no syntax error. Otherwise, it is dying, so at least that aspect of the problem is managed.
Because the lookup, itself is failing, so the when condition fails and your tasks is not failing as it normally should.
A way around this failure would be to use the default of the vars lookup, but from there it all depends what are your acceptable values for those variables.
An example, considering I do not want blank string in them would be:
- name: Check for required variables
fail:
msg: "variable '{{item}}' is not defined"
when: lookup('vars', item, default='') == ''
loop: "{{ required_vars }}"
vars:
required_vars:
- 'varname1'
- 'varname2'
Which would yield your expected:
TASK [Check for required variables] **************************************
failed: [localhost] (item=varname1) => changed=false
ansible_loop_var: item
item: varname1
msg: variable 'varname1' is not defined
failed: [localhost] (item=varname2) => changed=false
ansible_loop_var: item
item: varname2
msg: variable 'varname2' is not defined
A better way, though, is to use the undef keyword, this way, if you do template the variable, they will automatically raise the undefined hint provided.
Given the playbook
- hosts: localhost
gather_facts: no
vars:
varname1: "{{ undef(hint='Please provide a value for `varname1`') }}"
tasks:
- debug:
msg: "{{ varname1 }}"
It yields:
atal: [localhost]: FAILED! =>
msg: |-
The task includes an option with an undefined variable.
The error was: {{ undef(hint='Please provide a value for `varname1`') }}:
Please provide a value for `varname1`
I need to fetch a value from a dictionary, that might be either defined or undefined, but I can't find a proper way.
Let's assume we have a dictionary:
- name: Define the dictionaty
set_fact:
my_dictionary:
my_key: my_value
Then let's assume that we had to drop the dictionary at some point in some case:
- name: Drop the dictionary
set_fact:
my_dictionary:
And then I need to check if the dictionary defined and assign a variable from one of this dictionary's keys:
- name: Fetch the parameter from the dictionary or set the default value
set_fact:
my_var: >-
(my_dictionary != None) | ternary(
{{ my_dictionary.my_key }},
'my_default_value'
)
Wrong move!
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'None' has no attribute 'my_key'\n\nThe error appears to be in '***/test.yml': line 16, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n - name: Fetch the parameter from the dictionary or set the default value\n ^ here\n"}
As I understood, Ansible tries to compute the values of the parameters of ternary() in any case. Well, I understand that Ansible is not a programming language, so I shouldn't expect that it will act like a programming language. :-) But, anyhow, how to fetch a value from a dictionaty if the dictionary might be set to None?
The only (ugly) way I've found is something like that:
- name: Fetch the parameter from the dictionary
set_fact:
my_var: "{{ my_dictionary.my_key }}"
when:
my_dictionary != None
- name: Set the default value
set_fact:
my_var: 'my_default_value'
when:
my_dictionary == None
Though I'd rather use some shorter form.
Would anyone be so kind as to share a hint, please?
That's what the default() filter is for.
- hosts: localhost
vars:
foo:
baz:
bar: not the default
tasks:
- debug:
msg: "{{ foo.bar | default('my_default_value') }}"
- debug:
msg: "{{ bar.bar | default('my_default_value') }}"
- debug:
msg: "{{ baz.bar | default('my_default_value') }}"
Output:
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "my_default_value"
}
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "my_default_value"
}
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "not the default"
}
Hope I understood all your requirements correctly but the code seems to be way more complicated that it needs to be.
How about define dict:
-set_fact:
my_dictionary:
1: one
If dict is defined grab the value
-set_fact:
my_var: "{{ my_dictionary.1 }}"
when: my_dictionary is defined
Else set it to value of your choice:
-set_fact:
my_var: two
when: my_dictionary is not defined
Slava Ukraini!
Let's assume we need to interpolate a value from one of two variables. If the first one is undefined, use the value of the second one.
- set_fact:
result: >-
var1 | default(var2)
Though we also can't accept that the result will be empty, so we need to make sure that at least one of var1 or var2 is defined (not both of them though). In this case we would be willing to do something like that:
- set_fact:
result: >-
var1 | default(var2 | mandatory)
It looks good, but it doesn't work. Alas, Ansible raises an exception if var2 in not defined even if var1 is fine. It doesn't matter whether var1 is defined or not, Ansible just checks if var2 defined without taking into consideration that this check is not needed if it actually isn't going to assign a default value.
Speaking frankly, as from my point of view, this var2 | mandatory should never be proceeded if var1 is defined, but Ansible is not a programming language, so I understand that it works another way.
The only idea I have is to make sure that one of variables is defined beforehand, so it looks as follows:
- assert:
that:
- (var1 | mandatory) or (var2 | mandatory)
- set_fact:
result: >-
var1 | default(var2)
Although I don't like that, because of 2 reasons, here they are.
I have to add a whole additional step (assert).
I don't like that var2 is not being checked in the set_fact step. Of course, Ansible checks them on the previous step, but it still looks like result might get the null value, which is undesired. Even if it only looks like that it still irritates me a little.
What I would like is to find some alternative to the mandatory filter, but I need this "quasi-mandatory" to be considered only if var1 is undefined.
Is there a way to solve it with some short and neat idiom instead of these 2 steps? Something like $var1 // $var2 // any_way_to_raise_an_exception('Neither $var1 nor #var2 is defined, accept our condolences'); in Perl. :-)
For the cases I tried, it works with an outer set of parens, and I also found that mandatory accepts an undocumented message for when it dies
- debug:
msg: the value is {{ ((var1|default(var2))|mandatory("need var1 or var2") }}
and gives precedence to var1, uses var2 if provided, and dies with fatal: [localhost]: FAILED! => {"msg": "need var1 or var2"} if neither are
The problem is your unnecessary introduction of mandatory. Ansible's behaviour is correct without this, unless you've (very unwisely) disabled error_on_undefined_vars
- hosts: localhost
vars:
foo: eh
bar: bee
tasks:
# foo is set, so we get its value
- debug:
msg: "{{ foo | default(bar) }}"
# baz is not set, so we get bar
- debug:
msg: "{{ baz | default(bar) }}"
# foo is set, so we get its value
- debug:
msg: "{{ foo | default(quux) }}"
# neither baz nor quux is set, so we get an error
- debug:
msg: "{{ baz | default(quux) }}"
Output:
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "eh"
}
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "bee"
}
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "eh"
}
TASK [debug] *******************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'quux' is undefined\n\nThe error appears to be in '/home/ec2-user/test.yml': line 20, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n # neither baz nor quux is set, so we get an error\n - debug:\n ^ here\n"}
If you have disabled error_on_undefined_vars, you should use mandatory as the final filter, instead of putting it on a non-mandatory value:
- debug:
msg: "{{ baz | default(quux) | mandatory }}"
Or, if you require that one and exactly one value be set, there's no way around using assert (because you're imposing a non-standard requirement), but you should use the defined test, not the mandatory filter.
- hosts: localhost
vars:
foo: eh
bar: bee
tasks:
- assert:
that: "[foo, baz] | select('defined') | length == 1"
- assert:
that: "[foo, bar] | select('defined') | length == 1"
ignore_errors: true
- assert:
that: "[baz, quux] | select('defined') | length == 1"
TASK [assert] ******************************************************************
ok: [localhost] => {
"changed": false,
"msg": "All assertions passed"
}
TASK [assert] ******************************************************************
fatal: [localhost]: FAILED! => {
"assertion": "[foo, bar] | select('defined') | length == 1",
"changed": false,
"evaluated_to": false,
"msg": "Assertion failed"
}
...ignoring
TASK [assert] ******************************************************************
fatal: [localhost]: FAILED! => {
"assertion": "[baz, quux] | select('defined') | length == 1",
"changed": false,
"evaluated_to": false,
"msg": "Assertion failed"
}
Sidenote: It's possible to declare a default value if both var1 and var2 are not defined. For example,
result: "{{ var1|default(var2)|default('default') }}"
But, the use-case of the question is: Fail if both var1 and var2 are not defined.
Q: "Can't accept that the result will be empty, so we need to make sure that at least one of var1 or var2 is defined."
A: assert does not work the way you expect. It will fail if var1 is not defined. For example,
- debug:
var: var1
- debug:
var: var2
- assert:
that: var1|mandatory or var2|mandatory
TASK [debug] *********************************************************************************
ok: [localhost] =>
var1: VARIABLE IS NOT DEFINED!
TASK [debug] *********************************************************************************
ok: [localhost] =>
var2: test
TASK [assert] ********************************************************************************
fatal: [localhost]: FAILED! =>
msg: 'The conditional check ''(var1|mandatory) or (var2|mandatory)'' failed. The error was: Mandatory variable ''var1'' not defined.'
Instead, the task below does what you want
- assert:
that: (var1|default('')|length > 0) or
(var2|default('')|length > 0)
Q: "Short and neat idiom instead of 2 steps? Something like exception('Neither $var1 nor #var2 is defined')."
A: Actually, the default filter does it. The exception below says: 'var2' is undefined. (We know that var2 is needed only if var1 is undefined. Why repeat it?)
- debug:
var: var1
- debug:
var: var2
- set_fact:
result: "{{ var1|default(var2) }}"
TASK [debug] *********************************************************************************
ok: [localhost] =>
var1: VARIABLE IS NOT DEFINED!
TASK [debug] *********************************************************************************
ok: [localhost] =>
var2: VARIABLE IS NOT DEFINED!
TASK [set_fact] ******************************************************************************
fatal: [localhost]: FAILED! =>
msg: |-
The task includes an option with an undefined variable. The error was: 'var2' is undefined
I load a file with the content Some {{bar}} random text using the file lookup plugin:
- name: Load foo
set_fact:
foo: "{{ lookup('file', 'foo.txt') }}"
- name: Output foo
debug:
msg: "{{foo}}"
If I output {{foo}}, the output is literally Some {{bar}} random text. Ignoring the fact that I could use the template lookup plugin instead: Is it possible to evaluate {{foo}} after the file has been loaded, so that the actual value of bar will be injected into Some {{bar}} random text?
I am looking for something like:
- name: Evaluate foo
set_fact:
evaulated_foo: "{{ lookup('template', foo) }}" #Use the value of foo instead of a file
This is the intended behaviour. Lookups return unsafe text, which will never be templated. Use the template lookup when you want to return the contents of a file after template evaluation.
To avoid ordering issues, use a variable with the lookup instead of set_fact. set_fact sets variables to a static, fully evaluated value while normal variables are evaluated lazily, so they don't require that all of the variables be defined at the same time.
- hosts: localhost
gather_facts: false
vars:
foo: "{{ lookup('template', 'test.j2') }}"
tasks:
# These will work because bar is a current variable
- debug:
msg: "{{ foo }}"
vars:
bar: lemon
- debug:
msg: "{{ foo }}"
vars:
bar: orange
# This will not work, because bar isn't set
- set_fact:
foo: "{{ foo }}"
PLAY [localhost] ***************************************************************
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "Some lemon random text\n"
}
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "Some orange random text\n"
}
TASK [set_fact] ****************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: {{ lookup('template', 'test.j2') }}: 'bar' is undefined\n\nThe error appears to be in '/home/ec2-user/test.yml': line 18, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n # This will not work, because bar isn't set\n - set_fact:\n ^ here\n"}
I need to use variable defined for some hosts (in inventory), on another host.
Here i define it in my inventory
[mygroup:vars]
service_url=my_front_url
Where mygroup contain other groups, containing my hosts.
Then my playbook :
- name: Get variable
hosts: 127.0.0.1
tasks:
- debug:
var: hostvars[groups['{{ platform }}'][0]]['service_url']
- debug:
msg: "{{ hostvars[groups['\"{{ platform }}\"'][0]]['service_url'] }}"
Where platform is an extra-var (setting which "mygroup" to use)
and where 127.0.0.1 is my ansible host, distinct from my target hosts.
ex:
ansible-playbook test.yaml --extra-vars='platform=my-group'
TASK [debug] ********************************************************************************************************************************************************************
ok: [127.0.0.1] => {
"hostvars[groups['idi_se_prod'][0]]['service_url']": "my-front-url"
}
TASK [debug] ********************************************************************************************************************************************************************
fatal: [127.0.0.1]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'dict object' has no attribute '\"{{ platform }}\"'\n\nThe error appears to have been in 'XXXX/ansible/test.yaml': line 6, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n var: hostvars[groups['{{ platform }}'][0]]['service_url']\n - debug:\n ^ here\n"}
If i set static group name in yaml, this work fine.
- name: Get variable
hosts: 127.0.0.1
tasks:
- debug:
var: hostvars[groups['{{ platform }}'][0]]['service_url']
- debug:
msg: "{{ hostvars[groups['mygroup'][0]]['service_url'] }}"
TASK [debug] ********************************************************************************************************************************************************************
ok: [127.0.0.1] => {
"hostvars[groups['my-group'][0]]['service_url']": "my-front-url"
}
TASK [debug] ********************************************************************************************************************************************************************
ok: [127.0.0.1] => {
"msg": "my_front_url"
}
It look like a syntax probleme but i tried so many ways that i think i could use some help.
Thank you
Nicolas
Everything inside {{ and }} is more or less just python, so don't use recursive templates like you have:
msg: "{{ hostvars[groups['\"{{ platform }}\"'][0]]['service_url'] }}"
instead just reference the variable like it is, a variable:
msg: "{{ hostvars[groups[platform][0]]['service_url'] }}"