How are Ansible group variable files parsed? - ansible

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.

Related

How to get the symlink target path in ansible

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) }}"

How can I make Ansible interpret a variable inside a variable?

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

How to access an ansible block variable in a file from the same block

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"

Overwrite ansible variable from file fails

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.

Using variables for file name and file contents in lineinfile module

I am trying to read the contents of a file, store these in a variable and then insert them into another file if they don't already exist.
So, how I'm attempting to go about this is as follows:
# Create a variable that represents the path to the file that you want to read from
ssh_public_key_file: '../../jenkins_master/files/{{ hostvars[inventory_hostname]["environment"] }}/id_rsa.pub'
# Create a variable that represents the contents of this file:
ssh_public_key: "{{ lookup('file', '{{ ssh_public_key_file }}') }}"
I then use these variables in my Ansible playbook as follows:
- name: Install SSH authorized key
lineinfile: create=yes dest=~/.ssh/authorized_keys line=" {{ ssh_public_key }}" mode=0644
However, when I try and run the playbook, I get the following error message:
could not locate file in lookup: {{ ssh_public_key_file }}
Can anyone recommend a solution or suggest what I may have done wrong?
Thanks,
Seán
You have to change the line to:
# Create a variable that represents the contents of this file:
ssh_public_key: "{{ lookup('file', ssh_public_key_file) }}"
If you need to concatenate variables and strings you can do it like this:
# Example with two variables
ssh_public_key: "{{ lookup('file', var_1+var_2) }}"
# Example with string and variable
ssh_public_key: "{{ lookup('file', '~/config/'+var_1) }}"
. .
First I would make sure that your ssh_public_key_file variable is set up properly. If you add a task like the following what does it show?
- name: display variable
debug: var=ssh_public_key_file
If the output looks something like this then the variable isn't defined properly (eg. the "environment" fact doesn't exist for the host):
ok: [localhost] => {
"ssh_public_key_file": "../../jenkins_master/files/{{ hostvars[inventory_hostname][\"environment\"] }}/id_rsa.pub"
}
However if everything is defined properly then your output should show the variables replaced with their correct values:
ok: [localhost] => {
"ssh_public_key_file": "../../jenkins_master/files/foo/id_rsa.pub"
}
Once you've verified that then I would do the same thing with your ssh_public_key variable. Just output its value using the debug module. It should display as the contents of the public key file.
One other thing I would strongly suggest is to avoid using lineinfile altogether. Since you're working with SSH keys I would recommend you use the authorized_key module instead. It's a much cleaner way of managing authorized_keys files.

Resources