Whenever I try to make Ansible interpret a nested variable — so, a variable inside another variable — I cannot get the result I expect.
Given the variables:
key: bar
foo:
bar: baz
foo_bar: baz
I have tried those three approaches without much luck to dynamically access the key bar of the dictionary foo or the key foo_bar, when constructed from the value of key:
- ansible.builtin.debug:
msg: "{{ foo[{{ key }}] }}"
But, I get the error:
'template error while templating string: expected token '':'', got ''}''. String: {{ foo[{{ key }}] }}'
- ansible.builtin.debug:
msg: "{{ foo_{{ key }} }}"
But, I get a similar error
'template error while templating string: expected token ''end of print statement'', got ''{''. String: {{ foo_{{ key }} }}'
- ansible.builtin.debug:
msg: "{{ foo['{{ key }}'] }}"
And here, I get the error
The task includes an option with an undefined variable. The error was: 'dict object' has no attribute '{{ key }}'
I was expecting to get the value of foo.bar or foo_bar, so baz.
What would be the correct approach to achieve this?
As advised in the Frequently Asked Questions of Ansible, moustache do not stack.
Another rule is ‘moustaches don’t stack’. We often see this:
{{ somevar_{{other_var}} }}
The above DOES NOT WORK as you expect, if you need to use a dynamic variable use the following as appropriate:
{{ hostvars[inventory_hostname]['somevar_' ~ other_var] }}
For ‘non host vars’ you can use the vars lookup plugin:
{{ lookup('vars', 'somevar_' ~ other_var) }}
Source: https://docs.ansible.com/ansible/latest/reference_appendices/faq.html#when-should-i-use-also-how-to-interpolate-variables-or-dynamic-variable-names
So, there are two cases where this will apply:
When trying to access a key of a dictionary from a variable, you would simply use the variable as is, remembering that, when you are inside the expression delimiters {{ ... }}, a string will be interpreted as a variable, if not enclosed inside simple or double quotes.
- ansible.builtin.debug:
msg: "{{ foo[key] }}"
vars:
key: bar
foo:
bar: baz
When trying to construct the name of the variable or the key of a dictionary from a variable, you will have to use the concatenation operator, ~:
- ansible.builtin.debug:
msg: "{{ foo['foo_' ~ key] }}"
vars:
key: bar
foo:
foo_bar: baz
You might also need to use the vars lookup to access a dynamic variable:
- ansible.builtin.debug:
msg: "{{ lookup('vars', 'foo_' ~ key) }}"
vars:
key: bar
foo_bar: baz
Side notes:
Do use the vars lookup — lookup('vars', 'somevar_' ~ other_var) — and not the vars dictionary — vars['somevar_' ~ other_var], as it was never intended to be an Ansible feature and will be removed in future version
Short history, vars is a leftover from previous code that used it to pass variables to template, it was never intended for external use and most of the time didn't template anything.
Unrelated changes allowed it to template 'sometimes' but this was never on purpose, the only reason it was not removed is because some people relied on it, that had discovered by looking at the code and/or other people that had already been using it. Even though it has been our intention for a long time to deprecate and remove the vars construct, lack of a good way to trigger a runtime message has kept us from doing so.
We created 2 alternatives via lookups varnames and vars, which might not be as flexible as a dict but also would not chew up memory for unneeded access, since most users just want to match a small subset of existing variables.
Source: https://github.com/ansible/ansible/issues/74904#issuecomment-854137949
It is more advisable to use the right concatenation operator, ~ than the math operator + as advised in the Ansible documentation for the reason raised in Jinja documentation:
Usually the objects are numbers, but if both are strings or lists, you can concatenate them this way. This, however, is not the preferred way to concatenate strings! For string concatenation, have a look-see at the ~ operator.
Source: https://jinja.palletsprojects.com/en/2.11.x/templates/#math
Related
Want to check if the symlink destination is having the folder name which is passed as extra vars. The below is not working:
- name: Print a debug message
debug:
msg: {{package_version}} contains ab_slink.stat.lnk_target
register: ab_slink_exist
when: "ab_slink.stat.lnk_target is search '{{package_version}}'"
getting this error:
\[WARNING\]: conditional statements should not include jinja2 templating
delimiters such as {{ }} or {% %}.
The contents of a when directive are evaluated in an implicit Jinja context -- that is, you can pretend they are already surrounded by {{...}} markers. You never nest {{...}} markers; if you want to reference a variable inside a Jinja expression, you just use the variable name. Your task should look something like this:
- name: Print a debug message
debug:
msg: "{{package_version}} contains ab_slink.stat.lnk_target"
register: ab_slink_exist
when: "ab_slink.stat.lnk_target is search(package_version)"
Except this is still problematic, because you don't generally use debug tasks to set values. I would write something like this:
- name: set ab_slink_exist
set_fact:
ab_slink_exist: "{{ ab_slink.stat.lnk_target is search(package_version) }}"
Found an odd behavior I can't explain;
Running a small Ansible Playbook to just gather and display operating system information from hosts(For example: "Debian GNU/Linux 10")
I randomly stumbled across this solution(using carrot >), but can't find an explanation why it works this way;
---
- name: Show Operating System version information
hosts: all
gather_subset: distribution_version
tasks:
- name: Display operating system facts
debug:
msg: >
{{ ansible_facts.lsb.description }}
Generates successful output;
ok: [orville.lan] => {
"msg": "Ubuntu 19.10"
}
But if changed to this (remove >);
---
- name: Show Operating System version information
hosts: all
gather_subset: distribution_version
tasks:
- name: Display operating system facts
debug:
msg: {{ ansible_facts.lsb.description }}
Then running the playbook generates this error sequence;
ERROR! We were unable to read either as JSON nor YAML, these are the errors we got from each:
JSON: No JSON object could be decoded
Syntax Error while loading YAML.
found unacceptable key (unhashable type: 'AnsibleMapping')
The error appears to be in '/home/eschin/Repositories/ansible-files/OperatingSystemReport.yml': line 9, column 17, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
debug:
msg: {{ ansible_facts.lsb.description }}
^ here
We could be wrong, but this one looks like it might be an issue with
missing quotes. Always quote template expression brackets when they
start a value. For instance:
with_items:
- {{ foo }}
Should be written as:
with_items:
- "{{ foo }}"
I understand that the carrot > treats all following indented lines as a single line, i.e, converts newlines to spaces, but what other "secret sauce" is going on that makes the former work, but not the latter?
SOLUTION:
Simply making the interpolation a string, which the carrot > does for you, makes the second example work. I thought I'd tried this, and was sure it didn't work... but tried it again, and it works now. Coding Imps must have been messing with me 0:)
So this works;
---
- name: Show Operating System version information
hosts: all
gather_subset: distribution_version
tasks:
- name: Display operating system facts
debug:
msg: "{{ ansible_facts.lsb.description }}"
{{}} is used for jinja2 template and to make that work, you need to put the template in a string within quote ' or ". So below should work:
msg: "{{ ansible_facts.lsb.description }}"
# Or, within a string without quote like below (better to use quote because the yaml processor implementation may differ for some special characters),
msg: The description is {{ ansible_facts.lsb.description }}
Now for,
I randomly stumbled across this solution(using carrot >), but can't
find an explanation why it works
> and | represents folded and literal multiline strings in yaml respectively. Since > already put the template within a string, it was working as you indicated on your question.
Have a look at basic YAML syntax too.
I define a yml variable files for ansible with the following structure:
appserver:
root_directory: C:\app
config_directory: '{{ root_directory }}\config'
it seems the second variable config_directory cannot be interpreted correctly, I get a VARIABLE NOT FOUND ERROR.
I tried with:
appserver:
root_directory: C:\app
config_directory: '{{ appserver.root_directory }}\config'
It does not work either, I have a very long trace of error, the most interesting part is :
recursive loop detected in template string:{{ appserver.root_directory }}\config
When I use double quotes instead of simple quotes,
appserver:
root_directory: C:\app
config_directory: "{{ appserver.root_directory }}\config"
I get the following error:
The offending line appears to be:
app_root: D:\WynsureEnvironments\Application
wynsure_root: "{{ appserver.root_directory }}\config"
^ here
We could be wrong, but this one looks like it might be an issue with
missing quotes. Always quote template expression brackets when they
start a value. For instance:
with_items:
- {{ foo }}
Should be written as:
with_items:
- "{{ foo }}"
When using variable blocks, how can I reuse variables to assign new variables?
Thanks!
You cannot use such a recursive jinja2 variable declaration in ansible.
Here are 2 (non exhaustive list) alternative solutions:
Don't use a hash. Prepend your vars names. You will typically find this type of naming conventions in e.g. reusable roles on ansible galaxy
appserver_root_directory: C:\app
appserver_config_directory: '{{ appserver_root_directory }}\config'
If you really need a hash of this kind, declare a "private" variable outside of your hash and reuse it inside.
_appserver_root: C:\app
appserver:
root_directory: "{{ _appserver_root }}"
config_directory: "{{ _appserver_root }}\config"
In particular, I have a group_vars/all.yml file, with essentially the following contents:
my_foo: asdf
my_bar: '{{ my_foo }}'
If I later, while templating a file in a playbook, such as:
- name: template a file
template:
src: something.j2
dest: '....'
And in that something.j2 file, I use {{ my_bar }}, I somehow get asdf. How?
My prior understanding was that files such as group_vars/all.yml where essentially parsed as parse_yaml(parse_and_evaluate_jinja2(contents_of_the_file)). But if this were true, the line my_bar: '{{ my_foo }}' would not correctly evaluate: either we'd get an error, since my_foo is undefined to Jinja, or some default text, like "undefined", or ''. It's like the YAML was being parsed at the same time as the the Jinja2, which seems unlikely. Is that really what Ansible does? Or am I missing something else entirely?
(I couldn't find any exact documentation on how variable files are parsed in Ansibles docs, so if they exist, links are appreciated.)
My prior understanding was that files such as group_vars/all.yml where essentially parsed as parse_yaml(parse_and_evaluate_jinja2(contents_of_the_file))
Wrong. Ansible does all evaluation in runtime, for example:
You tell Ansible:
- debug:
msg: "{{ my_bar }}"
So when Ansible is going to pass msg parameter to debug module, it will do:
What is the parameter?
The parameter is {{ my_bar }}. Oh, it's a template and there's variable my_bar.
What is the my_bar's value?
The value is {{ my_foo }}. Oh, it's a template again, and there's variable my_foo.
What is the my_foo's value?
The value is asdf. Good! Nothing more to template.
So, my_bar's value is now asdf.
And parameter value is not asdf.
Let's print asdf.
By writing:
my_var: "{{ my_other_var }}"
you don't assign variables as in general programming, but write a simple template to calculate my_var's value in any given time (runtime), and not in time of parsing.
I'm trying to overwrite a previously defined variable my_var (when it's set to LATEST) by loading a value from a file (containing, say, NEWVALUE).
- name: Load from file
vars:
my_var: "{{ lookup('file', '~/file.txt') }}"
my_var2: "{{ lookup('file', '~/file.txt') }}"
debug: msg="my_var is {{ my_var }} my_var2 is {{ my_var2 }}"
when: "{{ my_var=='LATEST' }}"
This prints
ok: [host] ==> {
"msg": "my_var is LATEST my_var2 is NEWVALUE"
}
So I feel that I've verified that I'm loading the value correctly.. but for some reason I can't set the result of lookup in a previously set variable. Disabling the when clause doesn't seem to make any difference.
Should I be able to do this? As an alternative I'm going to use a third variable and just set it to either the preexisting value or the value from the file - but this seems like an unnecessary step to me.
Ansible version 2.1.0.0 b.t.w.
The vars you defined in your example only are available for the single debug task.
As you mentioned in the comment you figured this out and used set_fact instead. And yes, this won't work if you have passed the same variable as extra-var, as it has the highest precedence. There is no way to override a variable you passed as extra-var.